Initial commit: ERP system with advance verification fixes
This commit is contained in:
@@ -0,0 +1,813 @@
|
||||
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';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
// 项目支出分类
|
||||
const PROJECT_EXPENSE_CATEGORIES = [
|
||||
{ value: 'material_purchase', label: '材料采购' },
|
||||
{ value: 'equipment_purchase', label: '设备采购' },
|
||||
{ value: 'pole_crossarm', label: '电杆横担支出' },
|
||||
{ value: 'freight', label: '运费支出' },
|
||||
{ value: 'construction', label: '施工费支出' },
|
||||
{ value: 'other', label: '其他支出' }
|
||||
];
|
||||
|
||||
// 公司支出分类
|
||||
const COMPANY_EXPENSE_CATEGORIES = [
|
||||
{ value: 'office_operations', label: '通用运营(房租/耗材)' },
|
||||
{ value: 'transportation', label: '交通通勤' },
|
||||
{ value: 'marketing', label: '业扩营销' },
|
||||
{ value: 'power_system', label: '电力系统关系' },
|
||||
{ value: 'employee_welfare', label: '员工福利' },
|
||||
{ value: 'logistics', label: '快递物流' },
|
||||
{ value: 'other', label: '其他支出' }
|
||||
];
|
||||
|
||||
const ApprovalManagement: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [detailModalVisible, setDetailModalVisible] = useState(false);
|
||||
const [editModalVisible, setEditModalVisible] = useState(false);
|
||||
const [historyModalVisible, setHistoryModalVisible] = useState(false);
|
||||
const [selectedRecord, setSelectedRecord] = useState<any>(null);
|
||||
const [approvalType, setApprovalType] = useState<'approve' | 'reject'>('approve');
|
||||
const [form] = Form.useForm();
|
||||
const [editForm] = Form.useForm();
|
||||
|
||||
// 审批记录
|
||||
const [approvalHistory, setApprovalHistory] = useState<any[]>([]);
|
||||
|
||||
// 待审批数据
|
||||
const [pendingData, setPendingData] = useState<any[]>([]);
|
||||
|
||||
// 已审批数据
|
||||
const [approvedData, setApprovedData] = useState<any[]>([]);
|
||||
|
||||
// 项目列表
|
||||
const [projects, setProjects] = useState<any[]>([]);
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
fetchPendingData();
|
||||
fetchProjects();
|
||||
fetchApprovalHistory();
|
||||
}, []);
|
||||
|
||||
// 获取审批历史记录
|
||||
const fetchApprovalHistory = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('开始获取审批历史记录');
|
||||
// 获取所有类型的申请记录
|
||||
const types = ['advances', 'reimbursements', 'payment-requests', 'verifications'];
|
||||
const historyData = [];
|
||||
|
||||
for (const type of types) {
|
||||
const response = await fetch(`http://localhost:3005/api/${type}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
data.data.forEach((item: any) => {
|
||||
// 对于预支申请,包含所有状态
|
||||
// 对于其他类型,保持原有逻辑
|
||||
if (type === 'advances' || item.status === 'approved' || item.status === 'rejected' || (type === 'verifications' && item.status === 'pending_edit')) {
|
||||
let typeText = '';
|
||||
let code = '';
|
||||
let date = '';
|
||||
let action = '';
|
||||
|
||||
switch (type) {
|
||||
case 'advances':
|
||||
typeText = '预支申请';
|
||||
code = item.advance_code;
|
||||
date = item.advance_date;
|
||||
// 根据预支申请的状态设置操作文本
|
||||
switch (item.status) {
|
||||
case 'pending':
|
||||
action = '待审批';
|
||||
break;
|
||||
case 'approved':
|
||||
action = '通过';
|
||||
break;
|
||||
case 'rejected':
|
||||
action = '退回';
|
||||
break;
|
||||
case 'executed':
|
||||
action = '已执行';
|
||||
break;
|
||||
case 'partial_verification':
|
||||
action = '部分核销';
|
||||
break;
|
||||
case 'completed':
|
||||
action = '已完结';
|
||||
break;
|
||||
default:
|
||||
action = item.status;
|
||||
}
|
||||
break;
|
||||
case 'reimbursements':
|
||||
typeText = '报销申请';
|
||||
code = item.reimbursement_code;
|
||||
date = item.reimbursement_date;
|
||||
action = item.status === 'approved' ? '通过' : '退回';
|
||||
break;
|
||||
case 'payment-requests':
|
||||
typeText = '付款申请';
|
||||
code = item.request_code;
|
||||
date = item.payment_date;
|
||||
action = item.status === 'approved' ? '通过' : '退回';
|
||||
break;
|
||||
case 'verifications':
|
||||
typeText = '核销申请';
|
||||
code = item.verification_code;
|
||||
date = item.verification_date;
|
||||
action = item.status === 'approved' ? '通过' : item.status === 'rejected' ? '退回' : '待编辑';
|
||||
break;
|
||||
}
|
||||
|
||||
historyData.push({
|
||||
id: `${type}-${item.id}`,
|
||||
applyCode: code,
|
||||
applyType: typeText,
|
||||
applicant: item.applicant,
|
||||
amount: item.amount,
|
||||
currency: item.currency,
|
||||
action: action,
|
||||
operator: '系统管理员', // 实际应该从数据库中获取
|
||||
remark: item.approval_remark || '',
|
||||
timestamp: date
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('审批历史记录:', historyData);
|
||||
setApprovalHistory(historyData);
|
||||
} catch (error) {
|
||||
console.error('获取审批历史记录失败:', error);
|
||||
message.error('获取审批历史记录失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取项目列表
|
||||
const fetchProjects = async () => {
|
||||
try {
|
||||
const response = await fetch('http://localhost:3005/api/projects');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && Array.isArray(data.data)) {
|
||||
setProjects(data.data);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取项目列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取待审批数据
|
||||
const fetchPendingData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('开始获取待审批数据');
|
||||
// 获取预支申请
|
||||
const advancesRes = await fetch('http://localhost:3005/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');
|
||||
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');
|
||||
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');
|
||||
console.log('Verifications response status:', verificationsRes.status);
|
||||
const verificationsData = await verificationsRes.json();
|
||||
console.log('Verifications data:', verificationsData);
|
||||
|
||||
// 合并数据
|
||||
const allPendingData = [];
|
||||
|
||||
// 添加预支申请
|
||||
if (advancesData.success && advancesData.data) {
|
||||
console.log('Advances data length:', advancesData.data.length);
|
||||
advancesData.data.forEach((item: any) => {
|
||||
console.log('Advance item:', item);
|
||||
if (item.status === 'pending') {
|
||||
allPendingData.push({
|
||||
key: `adv-${item.id}`,
|
||||
id: item.id,
|
||||
type: '预支申请',
|
||||
code: item.advance_code,
|
||||
applicant: item.applicant,
|
||||
amount: item.amount,
|
||||
currency: item.currency,
|
||||
date: item.advance_date,
|
||||
reason: item.reason,
|
||||
status: item.status,
|
||||
rawData: item
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加报销申请
|
||||
if (reimbursementsData.success && reimbursementsData.data) {
|
||||
console.log('Reimbursements data length:', reimbursementsData.data.length);
|
||||
reimbursementsData.data.forEach((item: any) => {
|
||||
console.log('Reimbursement item:', item);
|
||||
if (item.status === 'pending') {
|
||||
allPendingData.push({
|
||||
key: `reimb-${item.id}`,
|
||||
id: item.id,
|
||||
type: '报销申请',
|
||||
code: item.reimbursement_code,
|
||||
applicant: item.applicant,
|
||||
amount: item.amount,
|
||||
currency: item.currency,
|
||||
date: item.reimbursement_date,
|
||||
reason: item.reason,
|
||||
status: item.status,
|
||||
rawData: item
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加付款申请
|
||||
if (paymentsData.success && paymentsData.data) {
|
||||
console.log('Payments data length:', paymentsData.data.length);
|
||||
paymentsData.data.forEach((item: any) => {
|
||||
console.log('Payment item:', item);
|
||||
if (item.status === 'pending') {
|
||||
allPendingData.push({
|
||||
key: `pay-${item.id}`,
|
||||
id: item.id,
|
||||
type: '付款申请',
|
||||
code: item.request_code,
|
||||
applicant: item.applicant,
|
||||
amount: item.amount,
|
||||
currency: item.currency,
|
||||
date: item.payment_date,
|
||||
reason: item.reason,
|
||||
status: item.status,
|
||||
rawData: item
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 添加核销申请
|
||||
if (verificationsData.success && verificationsData.data) {
|
||||
console.log('Verifications data length:', verificationsData.data.length);
|
||||
verificationsData.data.forEach((item: any) => {
|
||||
console.log('Verification item:', item);
|
||||
if (item.status === 'pending') {
|
||||
allPendingData.push({
|
||||
key: `ver-${item.id}`,
|
||||
id: item.id,
|
||||
type: '核销申请',
|
||||
code: item.verification_code,
|
||||
applicant: item.applicant,
|
||||
amount: item.amount,
|
||||
currency: item.currency,
|
||||
date: item.verification_date,
|
||||
reason: item.reason,
|
||||
status: item.status,
|
||||
rawData: item
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Final pending data:', allPendingData);
|
||||
setPendingData(allPendingData);
|
||||
} catch (error) {
|
||||
console.error('获取待审批数据失败:', error);
|
||||
message.error('获取待审批数据失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount: number, currency: string = 'CNY') => {
|
||||
const symbols: Record<string, string> = { CNY: '¥', USD: '$', LAK: '₭', THB: '฿' };
|
||||
return (symbols[currency] || '¥') + (amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
};
|
||||
|
||||
// 获取类型标签
|
||||
const getTypeTag = (type: string) => {
|
||||
const colors: Record<string, string> = { '预支申请': 'blue', '报销申请': 'green', '付款申请': 'orange', '核销申请': 'purple' };
|
||||
return <Tag color={colors[type] || 'default'}>{type}</Tag>;
|
||||
};
|
||||
|
||||
// 获取状态标签
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
pending: { color: 'processing', text: '待审批' },
|
||||
approved: { color: 'success', text: '已通过' },
|
||||
rejected: { color: 'error', text: '已退回' },
|
||||
withdrawn: { color: 'default', text: '已撤回' }
|
||||
};
|
||||
const config = statusMap[status] || { color: 'default', text: status };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
};
|
||||
|
||||
// 根据项目ID获取项目名称
|
||||
const getProjectName = (projectId: any) => {
|
||||
if (!projectId) return '-';
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
return project ? project.name : `项目ID: ${projectId}`;
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (record: any) => {
|
||||
setSelectedRecord(record);
|
||||
setApprovalType('approve');
|
||||
form.resetFields();
|
||||
setDetailModalVisible(true);
|
||||
};
|
||||
|
||||
// 处理审批通过
|
||||
const handleApprove = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 构建API请求URL
|
||||
const isAdvance = selectedRecord.key.startsWith('adv-');
|
||||
const isReimbursement = selectedRecord.key.startsWith('reimb-');
|
||||
const isPayment = selectedRecord.key.startsWith('pay-');
|
||||
const isVerification = selectedRecord.key.startsWith('ver-');
|
||||
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`;
|
||||
|
||||
// 发送API请求
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
// 从待审批列表中移除该申请
|
||||
setPendingData(pendingData.filter(item => item.key !== selectedRecord.key));
|
||||
message.success(`审批通过:${selectedRecord.code}`);
|
||||
setDetailModalVisible(false);
|
||||
} else {
|
||||
message.error(result.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('审批操作失败:', error);
|
||||
message.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理审批退回
|
||||
const handleReject = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 构建API请求URL
|
||||
const isAdvance = selectedRecord.key.startsWith('adv-');
|
||||
const isReimbursement = selectedRecord.key.startsWith('reimb-');
|
||||
const isPayment = selectedRecord.key.startsWith('pay-');
|
||||
const isVerification = selectedRecord.key.startsWith('ver-');
|
||||
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`;
|
||||
|
||||
// 发送API请求
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(values)
|
||||
});
|
||||
|
||||
const result = await res.json();
|
||||
if (result.success) {
|
||||
// 从待审批列表中移除该申请
|
||||
setPendingData(pendingData.filter(item => item.key !== selectedRecord.key));
|
||||
message.success(`已退回:${selectedRecord.code}`);
|
||||
setDetailModalVisible(false);
|
||||
} else {
|
||||
message.error(result.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('审批操作失败:', error);
|
||||
message.error('操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理撤回申请
|
||||
const handleWithdraw = (record: any) => {
|
||||
Modal.confirm({
|
||||
title: '撤回申请',
|
||||
content: `确认撤回申请 ${record.code} 吗?`,
|
||||
okText: '确认撤回',
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
setPendingData(pendingData.filter(item => item.key !== record.key));
|
||||
message.success('申请已撤回');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 处理编辑申请
|
||||
const handleEdit = (record: any) => {
|
||||
setSelectedRecord(record);
|
||||
editForm.setFieldsValue({ amount: record.amount, reason: record.reason });
|
||||
setEditModalVisible(true);
|
||||
};
|
||||
|
||||
// 处理编辑提交
|
||||
const handleEditSubmit = () => {
|
||||
editForm.validateFields().then(values => {
|
||||
message.success('修改成功,已重新提交审批');
|
||||
setEditModalVisible(false);
|
||||
});
|
||||
};
|
||||
|
||||
// 获取申请类型对应的API端点
|
||||
const getApiEndpoint = (key: string) => {
|
||||
if (key.startsWith('adv-')) return 'advances';
|
||||
if (key.startsWith('reimb-')) return 'reimbursements';
|
||||
if (key.startsWith('pay-')) return 'payment-requests';
|
||||
if (key.startsWith('ver-')) return 'verifications';
|
||||
return '';
|
||||
};
|
||||
|
||||
// 渲染附件列表
|
||||
const renderAttachments = (attachments: any) => {
|
||||
// 处理字符串类型的 attachments(JSON字符串)
|
||||
let attachmentList = attachments;
|
||||
if (typeof attachments === 'string') {
|
||||
try {
|
||||
attachmentList = JSON.parse(attachments);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保是数组
|
||||
if (!Array.isArray(attachmentList) || attachmentList.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{attachmentList.map((url: string, index: number) => (
|
||||
<div key={index} style={{ position: 'relative' }}>
|
||||
{url && url.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? (
|
||||
<img
|
||||
src={url}
|
||||
alt={`附件${index + 1}`}
|
||||
style={{ width: 120, height: 120, objectFit: 'cover', borderRadius: 4, border: '1px solid #f0f0f0', cursor: 'pointer' }}
|
||||
onClick={() => window.open(url, '_blank')}
|
||||
/>
|
||||
) : (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
<div style={{ width: 120, height: 120, display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #f0f0f0', borderRadius: 4, background: '#f5f5f5' }}>
|
||||
<FileImageOutlined style={{ fontSize: 32, color: '#999' }} />
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 获取支出分类的中文名称
|
||||
const getCategoryName = (category: string) => {
|
||||
if (!category) return '-';
|
||||
// 特殊分类映射
|
||||
const specialCategories: Record<string, string> = {
|
||||
// Project expense categories
|
||||
accommodation: '住宿',
|
||||
food: '餐饮',
|
||||
fuel: '加油',
|
||||
materials: '零散材料',
|
||||
customer_relations: '客户关系',
|
||||
subcontract_relations: '分包关系',
|
||||
edl_relations: 'EDL关系',
|
||||
extra_construction: '额外施工',
|
||||
// Company expense categories
|
||||
general_operations: '通用运营(房租/耗材)',
|
||||
transportation: '交通通勤',
|
||||
business_expansion: '业扩营销',
|
||||
power_system_relations: '电力系统关系',
|
||||
employee_benefits: '员工福利',
|
||||
express_logistics: '快递物流',
|
||||
other: '其他'
|
||||
};
|
||||
// 先检查特殊分类
|
||||
if (specialCategories[category]) return specialCategories[category];
|
||||
// 再从项目支出分类中查找
|
||||
const projectCategory = PROJECT_EXPENSE_CATEGORIES.find(c => c.value === category);
|
||||
if (projectCategory) return projectCategory.label;
|
||||
// 再从公司支出分类中查找
|
||||
const companyCategory = COMPANY_EXPENSE_CATEGORIES.find(c => c.value === category);
|
||||
if (companyCategory) return companyCategory.label;
|
||||
// 如果都找不到,返回原始值
|
||||
return category;
|
||||
};
|
||||
|
||||
// 渲染明细清单
|
||||
const renderDetailItems = (detailItems: any) => {
|
||||
// 处理字符串类型的 detailItems(JSON字符串)
|
||||
let itemsList = detailItems;
|
||||
if (typeof detailItems === 'string') {
|
||||
try {
|
||||
itemsList = JSON.parse(detailItems);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保是数组
|
||||
if (!Array.isArray(itemsList) || itemsList.length === 0) return null;
|
||||
|
||||
return (
|
||||
<List
|
||||
size="small"
|
||||
bordered
|
||||
dataSource={itemsList}
|
||||
renderItem={(item: any, index: number) => (
|
||||
<List.Item>
|
||||
<div style={{ width: '100%' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<span><strong>明细 {index + 1}:</strong> {item.description || getCategoryName(item.category) || '-'}</span>
|
||||
<span style={{ fontWeight: 'bold', color: '#1890ff' }}>{formatAmount(item.amount, item.currency)}</span>
|
||||
</div>
|
||||
{item.category && (
|
||||
<div style={{ marginBottom: 8, fontSize: 13, color: '#666' }}>
|
||||
<strong>支出分类:</strong>{getCategoryName(item.category)}
|
||||
</div>
|
||||
)}
|
||||
{item.attachments && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<span style={{ color: '#666', fontSize: 12 }}>明细附件:</span>
|
||||
{renderAttachments(item.attachments)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// 待审批列
|
||||
const pendingColumns = [
|
||||
{ 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) },
|
||||
{ title: '申请人', dataIndex: 'applicant', key: 'applicant', width: 80 },
|
||||
{ title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: (v: number, r: any) => formatAmount(v, r.currency) },
|
||||
{ title: '申请日期', dataIndex: 'date', key: 'date', width: 100 },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (v: string) => getStatusTag(v) },
|
||||
{ title: '编号', dataIndex: 'code', key: 'code', width: 140 },
|
||||
{
|
||||
title: '操作', key: 'action', width: 200,
|
||||
render: (_: any, record: any) => (
|
||||
<Space wrap>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
|
||||
// 审批记录列
|
||||
const historyColumns = [
|
||||
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 140 },
|
||||
{ title: '操作', dataIndex: 'action', key: 'action', width: 100 },
|
||||
{ title: '申请编号', dataIndex: 'applyCode', key: 'applyCode', width: 140 },
|
||||
{ title: '类型', dataIndex: 'applyType', key: 'applyType', width: 100, render: (v: string) => getTypeTag(v) },
|
||||
{ title: '申请人', dataIndex: 'applicant', key: 'applicant', width: 80 },
|
||||
{ title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: (v: number, r: any) => formatAmount(v, r.currency) },
|
||||
{ title: '操作人', dataIndex: 'operator', key: 'operator', width: 100 },
|
||||
{ title: '备注/原因', dataIndex: 'remark', key: 'remark', ellipsis: true }
|
||||
];
|
||||
|
||||
const tabItems = [
|
||||
{ key: 'pending', label: <span>待审批 <Badge count={pendingData.length} style={{ marginLeft: 8 }} /></span>, children: <Table columns={pendingColumns} dataSource={pendingData} rowKey="key" loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 1200 }} /> },
|
||||
{ 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 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<h2>审批管理</h2>
|
||||
<Button type="primary" onClick={fetchPendingData} loading={loading}>
|
||||
刷新数据
|
||||
</Button>
|
||||
</div>
|
||||
<p style={{ color: '#888', marginBottom: 0 }}>审批预支、报销、付款等申请</p>
|
||||
</div>
|
||||
<Card><Tabs items={tabItems} /></Card>
|
||||
|
||||
{/* 详情模态框 */}
|
||||
<Modal
|
||||
title={`${selectedRecord?.type}详情:${selectedRecord?.code}`}
|
||||
open={detailModalVisible}
|
||||
onCancel={() => setDetailModalVisible(false)}
|
||||
width={900}
|
||||
footer={
|
||||
selectedRecord?.status === 'pending' ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<Button onClick={() => setDetailModalVisible(false)}>取消</Button>
|
||||
<Button danger icon={<CloseOutlined />} onClick={handleReject}>退回</Button>
|
||||
<Button type="primary" icon={<CheckOutlined />} onClick={handleApprove}>通过</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Button onClick={() => setDetailModalVisible(false)}>关闭</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
{fullDetail && (
|
||||
<>
|
||||
{/* 基本信息 */}
|
||||
<Descriptions bordered column={2} size="small">
|
||||
<Descriptions.Item label="申请类型">{getTypeTag(selectedRecord.type)}</Descriptions.Item>
|
||||
<Descriptions.Item label="申请编号">{selectedRecord.code}</Descriptions.Item>
|
||||
<Descriptions.Item label="申请人">{selectedRecord.applicant}</Descriptions.Item>
|
||||
<Descriptions.Item label="申请日期">{selectedRecord.date}</Descriptions.Item>
|
||||
<Descriptions.Item label="金额">
|
||||
{formatAmount(selectedRecord.amount, selectedRecord.currency)}
|
||||
{selectedRecord.currency !== 'CNY' && fullDetail.amount_cny > 0 && (
|
||||
<span style={{ color: '#999', marginLeft: 8 }}>≈ ¥{fullDetail.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}</span>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{getStatusTag(selectedRecord.status)}</Descriptions.Item>
|
||||
<Descriptions.Item label="事由" span={2}>{selectedRecord.reason}</Descriptions.Item>
|
||||
|
||||
{/* 付款申请特有字段 */}
|
||||
{selectedRecord.type === '付款申请' && (
|
||||
<>
|
||||
<Descriptions.Item label="收款单位类型">
|
||||
{fullDetail.payee_type === 'subcontractor' ? '分包商' :
|
||||
fullDetail.payee_type === 'supplier' ? '供应商' :
|
||||
fullDetail.payee_type === 'customer' ? '客户' : '其他'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="收款方">{fullDetail.payee || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="银行名称">{fullDetail.bank_name || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="银行账号">{fullDetail.bank_account || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="支出类型">
|
||||
{fullDetail.expense_type === 'company' ? '公司支出' : '项目支出'}
|
||||
</Descriptions.Item>
|
||||
{fullDetail.expense_type === 'project' && fullDetail.project_id && (
|
||||
<Descriptions.Item label="关联项目">{getProjectName(fullDetail.project_id)}</Descriptions.Item>
|
||||
)}
|
||||
<Descriptions.Item label="支出分类">
|
||||
{fullDetail.expense_type === 'project'
|
||||
? (PROJECT_EXPENSE_CATEGORIES.find(c => c.value === fullDetail.expense_category)?.label || fullDetail.expense_category)
|
||||
: (COMPANY_EXPENSE_CATEGORIES.find(c => c.value === fullDetail.expense_category)?.label || fullDetail.expense_category)
|
||||
}
|
||||
</Descriptions.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 报销申请特有字段 */}
|
||||
{selectedRecord.type === '报销申请' && fullDetail.expense_type && (
|
||||
<>
|
||||
<Descriptions.Item label="支出类型">
|
||||
{fullDetail.expense_type === 'company' ? '公司支出' : '项目支出'}
|
||||
</Descriptions.Item>
|
||||
{fullDetail.project_id && (
|
||||
<Descriptions.Item label="关联项目">{getProjectName(fullDetail.project_id)}</Descriptions.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 核销申请特有字段 */}
|
||||
{selectedRecord.type === '核销申请' && fullDetail.advance_code && (
|
||||
<>
|
||||
<Descriptions.Item label="关联预支单">{fullDetail.advance_code}</Descriptions.Item>
|
||||
<Descriptions.Item label="预支金额">{formatAmount(fullDetail.advance_amount, fullDetail.currency)}</Descriptions.Item>
|
||||
<Descriptions.Item label="结算核销">
|
||||
<span style={{ fontWeight: 'bold', color: fullDetail.settlement ? '#52c41a' : '#fa8c16' }}>
|
||||
{fullDetail.settlement ? '是' : '否'}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="已核销金额">{formatAmount(fullDetail.total_reimbursed || 0, fullDetail.currency)}</Descriptions.Item>
|
||||
<Descriptions.Item label="剩余核销金额">{formatAmount((fullDetail.advance_amount || 0) - (fullDetail.total_reimbursed || 0), fullDetail.currency)}</Descriptions.Item>
|
||||
{fullDetail.settlement && fullDetail.settlement_amount && (
|
||||
<Descriptions.Item label="核销结算金额" span={2}>
|
||||
{fullDetail.settlement_amount > 0 ? `退款 ${formatAmount(fullDetail.settlement_amount, fullDetail.currency)}` : `补款 ${formatAmount(Math.abs(fullDetail.settlement_amount), fullDetail.currency)}`}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Descriptions>
|
||||
|
||||
{/* 明细清单 */}
|
||||
{fullDetail.detail_items && fullDetail.detail_items.length > 0 && (
|
||||
<>
|
||||
<Divider>明细清单</Divider>
|
||||
{renderDetailItems(fullDetail.detail_items)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 凭证附件或退款凭证 */}
|
||||
{fullDetail.attachments && fullDetail.attachments.length > 0 && (
|
||||
<>
|
||||
<Divider>{fullDetail.settlement && fullDetail.settlement_amount > 0 ? '退款凭证' : '凭证附件'}</Divider>
|
||||
{renderAttachments(fullDetail.attachments)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 审批备注表单 */}
|
||||
{selectedRecord.status === 'pending' && (
|
||||
<>
|
||||
<Divider>审批意见</Divider>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="remark" label="审批备注">
|
||||
<TextArea rows={3} placeholder="可选:填写审批备注" />
|
||||
</Form.Item>
|
||||
<Form.Item name="rejectReason" label="退回原因" style={{ display: 'none' }}>
|
||||
<TextArea rows={3} placeholder="请填写退回原因" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal title={`编辑申请:${selectedRecord?.code}`} open={editModalVisible} onCancel={() => setEditModalVisible(false)} onOk={handleEditSubmit} width={600}>
|
||||
<Form form={editForm} layout="vertical">
|
||||
<Form.Item label="申请类型"><Input value={selectedRecord?.type} disabled /></Form.Item>
|
||||
<Form.Item label="申请人"><Input value={selectedRecord?.applicant} disabled /></Form.Item>
|
||||
<Form.Item name="amount" label="金额" rules={[{ required: true }]}><Input type="number" style={{ width: '100%' }} /></Form.Item>
|
||||
<Form.Item name="reason" label="事由" rules={[{ required: true }]}><TextArea rows={3} /></Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal title={`${selectedRecord?.advance_code ? '预支申请' : '报销申请'}详情:${selectedRecord?.advance_code || selectedRecord?.reimbursement_code}`} open={historyModalVisible} onCancel={() => setHistoryModalVisible(false)} footer={null} width={800}>
|
||||
{selectedRecord && (
|
||||
<>
|
||||
<Descriptions bordered column={2} size="small">
|
||||
<Descriptions.Item label="申请编号">{selectedRecord.advance_code || selectedRecord.reimbursement_code}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{getStatusTag(selectedRecord.status)}</Descriptions.Item>
|
||||
<Descriptions.Item label="申请人">{selectedRecord.applicant}</Descriptions.Item>
|
||||
<Descriptions.Item label="申请日期">{selectedRecord.advance_date || selectedRecord.reimbursement_date}</Descriptions.Item>
|
||||
<Descriptions.Item label="金额">
|
||||
{formatAmount(selectedRecord.amount, selectedRecord.currency)}
|
||||
{selectedRecord.currency !== 'CNY' && selectedRecord.amount_cny && (
|
||||
<span style={{ color: '#999', marginLeft: 8 }}>≈ ¥{selectedRecord.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}</span>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="事由" span={2}>{selectedRecord.reason}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
{Array.isArray(selectedRecord.attachments) && selectedRecord.attachments.length > 0 && (
|
||||
<>
|
||||
<Divider>凭证附件</Divider>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{selectedRecord.attachments.map((url: string, index: number) => (
|
||||
<img key={index} src={url} width={100} height={100} style={{ objectFit: 'cover', borderRadius: 4, border: '1px solid #f0f0f0' }} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ApprovalManagement;
|
||||
Reference in New Issue
Block a user