640 lines
28 KiB
TypeScript
640 lines
28 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { Card, Table, Tag, Button, Space, Modal, Form, Input, Select, DatePicker, message, Tabs, Badge, Descriptions, Divider, List, Upload } from 'antd';
|
||
import { CheckOutlined, CloseOutlined, EyeOutlined, DollarOutlined, EditOutlined, UndoOutlined, ClockCircleOutlined, FileImageOutlined, UploadOutlined } from '@ant-design/icons';
|
||
import dayjs from 'dayjs';
|
||
|
||
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: '其他支出' }
|
||
];
|
||
|
||
// 执行记录类型
|
||
interface ExecutionRecord {
|
||
id: string;
|
||
applyCode: string;
|
||
applyType: string;
|
||
applicant: string;
|
||
amount: number;
|
||
currency: string;
|
||
action: 'execute' | 'reject';
|
||
operator: string;
|
||
operatorRole: string;
|
||
timestamp: string;
|
||
executeMethod?: string;
|
||
voucherNo?: string;
|
||
rejectReason?: string;
|
||
remark?: string;
|
||
}
|
||
|
||
const ExecutionManagement: 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 [executionType, setExecutionType] = useState<'execute' | 'reject'>('execute');
|
||
const [form] = Form.useForm();
|
||
const [editForm] = Form.useForm();
|
||
const [voucherFiles, setVoucherFiles] = useState<any[]>([]);
|
||
|
||
// 执行记录
|
||
const [executionHistory, setExecutionHistory] = useState<ExecutionRecord[]>([]);
|
||
|
||
// 待执行数据
|
||
const [pendingData, setPendingData] = useState([]);
|
||
|
||
// 已执行数据
|
||
const [executedData, setExecutedData] = useState([]);
|
||
|
||
// 从后端获取待执行数据
|
||
useEffect(() => {
|
||
const fetchPendingData = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const response = await fetch('http://localhost:3005/api/executions/pending');
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success && Array.isArray(data.data)) {
|
||
setPendingData(data.data.map((item: any, index: number) => ({
|
||
...item,
|
||
key: item.id || index,
|
||
rawData: item
|
||
})));
|
||
} else {
|
||
message.error('获取待执行数据失败:数据格式错误');
|
||
}
|
||
} else {
|
||
message.error('获取待执行数据失败:' + response.statusText);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取待执行数据错误:', error);
|
||
message.error('网络错误,获取待执行数据失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchPendingData();
|
||
}, []);
|
||
|
||
// 从后端获取已执行数据
|
||
useEffect(() => {
|
||
const fetchExecutedData = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const response = await fetch('http://localhost:3005/api/executions/executed');
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success && Array.isArray(data.data)) {
|
||
setExecutedData(data.data.map((item: any, index: number) => ({
|
||
...item,
|
||
key: item.id || index,
|
||
rawData: item
|
||
})));
|
||
} else {
|
||
message.error('获取已执行数据失败:数据格式错误');
|
||
}
|
||
} else {
|
||
message.error('获取已执行数据失败:' + response.statusText);
|
||
}
|
||
} catch (error) {
|
||
console.error('获取已执行数据错误:', error);
|
||
message.error('网络错误,获取已执行数据失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchExecutedData();
|
||
}, []);
|
||
|
||
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 formatNumberWithSeparator = (value: number | undefined, currency: string): string => {
|
||
if (value === undefined || value === null) return '';
|
||
const symbols: Record<string, string> = { CNY: '¥', USD: '$', LAK: '₭', THB: '฿' };
|
||
return (symbols[currency] || '¥') + value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||
};
|
||
|
||
const parseFormattedNumber = (value: string): number => {
|
||
const cleaned = value.replace(/[¥$₭฿,]/g, '');
|
||
return parseFloat(cleaned) || 0;
|
||
};
|
||
|
||
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: '待执行' },
|
||
executed: { color: 'success', text: '已执行' },
|
||
rejected: { color: 'error', text: '已退回' },
|
||
};
|
||
const config = statusMap[status] || { color: 'default', text: status };
|
||
return <Tag color={config.color}>{config.text}</Tag>;
|
||
};
|
||
|
||
// 添加执行记录
|
||
const addExecutionRecord = (record: any, action: 'execute' | 'reject', operator: string, operatorRole: string, data?: any) => {
|
||
const newRecord: ExecutionRecord = {
|
||
id: Date.now().toString(),
|
||
applyCode: record.code,
|
||
applyType: record.type,
|
||
applicant: record.applicant,
|
||
amount: record.amount,
|
||
currency: record.currency,
|
||
action,
|
||
operator,
|
||
operatorRole,
|
||
timestamp: dayjs().format('YYYY-MM-DD HH:mm'),
|
||
executeMethod: data?.executeMethod,
|
||
voucherNo: data?.voucherNo,
|
||
rejectReason: data?.rejectReason,
|
||
remark: data?.remark
|
||
};
|
||
setExecutionHistory([newRecord, ...executionHistory]);
|
||
};
|
||
|
||
// 查看详情
|
||
const handleViewDetail = (record: any) => {
|
||
setSelectedRecord(record);
|
||
setExecutionType('execute');
|
||
form.resetFields();
|
||
form.setFieldsValue({ execute_date: dayjs(), execute_method: 'bank' });
|
||
setVoucherFiles([]);
|
||
setDetailModalVisible(true);
|
||
};
|
||
|
||
// 处理执行
|
||
const handleExecute = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
|
||
// 检查是否上传了付款凭证
|
||
if (!voucherFiles || voucherFiles.length === 0) {
|
||
message.error('请上传付款凭证');
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
|
||
// 获取已上传文件的URL列表
|
||
const voucherFileUrls = voucherFiles
|
||
.map(f => f.url || f.response?.data?.url || f.response?.url)
|
||
.filter(url => url); // 过滤掉空值
|
||
|
||
console.log('上传的凭证文件:', voucherFiles);
|
||
console.log('凭证文件URL列表:', voucherFileUrls);
|
||
|
||
// 调用执行API
|
||
const executeResponse = await fetch('http://localhost:3005/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',
|
||
action: 'execute',
|
||
execute_method: values.execute_method,
|
||
voucher_files: voucherFileUrls,
|
||
remark: values.remark
|
||
})
|
||
});
|
||
|
||
if (executeResponse.ok) {
|
||
addExecutionRecord(selectedRecord, 'execute', '系统管理员', '管理员', {
|
||
executeMethod: values.execute_method === 'bank' ? '银行转账' : values.execute_method === 'cash' ? '现金' : '其他',
|
||
remark: values.remark
|
||
});
|
||
setPendingData(pendingData.filter(item => item.key !== selectedRecord.key));
|
||
message.success(`执行成功:${selectedRecord.code}`);
|
||
setDetailModalVisible(false);
|
||
} else {
|
||
message.error('执行操作失败,请重试');
|
||
}
|
||
} catch (error) {
|
||
console.error('执行操作失败:', error);
|
||
message.error('网络错误,操作失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 处理退回
|
||
const handleReject = async () => {
|
||
try {
|
||
const values = await form.validateFields();
|
||
|
||
setLoading(true);
|
||
|
||
// 调用退回API
|
||
const rejectResponse = await fetch('http://localhost:3005/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',
|
||
action: 'reject',
|
||
reject_reason: values.rejectReason
|
||
})
|
||
});
|
||
|
||
if (rejectResponse.ok) {
|
||
addExecutionRecord(selectedRecord, 'reject', '系统管理员', '管理员', { rejectReason: values.rejectReason });
|
||
setPendingData(pendingData.filter(item => item.key !== selectedRecord.key));
|
||
message.success(`已退回:${selectedRecord.code},申请人可编辑后重新提交`);
|
||
setDetailModalVisible(false);
|
||
} else {
|
||
message.error('退回操作失败,请重试');
|
||
}
|
||
} catch (error) {
|
||
console.error('退回操作失败:', error);
|
||
message.error('网络错误,操作失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleViewHistory = (record: any) => {
|
||
setSelectedRecord(record);
|
||
setHistoryModalVisible(true);
|
||
};
|
||
|
||
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);
|
||
});
|
||
};
|
||
|
||
// 渲染附件列表
|
||
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 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 || item.category || '-'}</span>
|
||
<span style={{ fontWeight: 'bold', color: '#1890ff' }}>{formatAmount(item.amount, item.currency)}</span>
|
||
</div>
|
||
{item.attachments && (
|
||
<div style={{ marginTop: 8 }}>
|
||
<span style={{ color: '#666', fontSize: 12 }}>明细附件:</span>
|
||
{renderAttachments(item.attachments)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</List.Item>
|
||
)}
|
||
/>
|
||
);
|
||
};
|
||
|
||
// 执行记录列
|
||
const historyColumns = [
|
||
{ title: '时间', dataIndex: 'timestamp', key: 'timestamp', width: 140 },
|
||
{ title: '操作', dataIndex: 'action', key: 'action', width: 100, render: (v: string) => {
|
||
const map: Record<string, { color: string; icon: any; text: string }> = {
|
||
execute: { color: 'green', icon: <CheckOutlined />, text: '执行' },
|
||
reject: { color: 'red', icon: <CloseOutlined />, text: '退回' }
|
||
};
|
||
const m = map[v] || { color: 'default', icon: null, text: v };
|
||
return <Tag color={m.color} icon={m.icon}>{m.text}</Tag>;
|
||
}},
|
||
{ 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: ExecutionRecord) => (
|
||
<>
|
||
<div>{formatAmount(v, r.currency)}</div>
|
||
{r.currency !== 'CNY' && r.amount_cny && <div style={{ color: '#999', fontSize: 12 }}>≈ ¥{r.amount_cny.toLocaleString('zh-CN', {minimumFractionDigits: 2})}</div>}
|
||
</>
|
||
) },
|
||
{ title: '执行方式', dataIndex: 'executeMethod', key: 'executeMethod', width: 100 },
|
||
{ title: '操作人', dataIndex: 'operator', key: 'operator', width: 100 },
|
||
{ title: '角色', dataIndex: 'operatorRole', key: 'operatorRole', width: 80 },
|
||
{ title: '退回原因', dataIndex: 'rejectReason', key: 'rejectReason', ellipsis: true },
|
||
];
|
||
|
||
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: 120, render: (v: number, r: any) => <span style={{ fontWeight: 'bold', color: '#1890ff' }}>{formatAmount(v, r.currency)}</span> },
|
||
{ title: '收款方', dataIndex: 'payee', key: 'payee', ellipsis: true, render: (v: string, r: any) => v || r.applicant },
|
||
{ title: '审批日期', dataIndex: 'approveDate', key: 'approveDate', width: 100 },
|
||
{ title: '编号', dataIndex: 'code', key: 'code', width: 140 },
|
||
{
|
||
title: '操作', key: 'action', width: 200,
|
||
render: (_: any, record: any) => (
|
||
<Space wrap>
|
||
<Button size="small" type="primary" icon={<DollarOutlined />} onClick={() => handleViewDetail(record)}>执行</Button>
|
||
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||
</Space>
|
||
)
|
||
}
|
||
];
|
||
|
||
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) },
|
||
{ title: '申请人', dataIndex: 'applicant', key: 'applicant', width: 80 },
|
||
{ title: '金额', dataIndex: 'amount', key: 'amount', width: 140, render: (v: number, r: any) => (
|
||
<>
|
||
<div>{formatAmount(v, r.currency)}</div>
|
||
{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 },
|
||
{ title: '执行方式', dataIndex: 'executeMethod', key: 'executeMethod', 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: 100,
|
||
render: (_: any, record: any) => (
|
||
<Button size="small" icon={<EyeOutlined />} onClick={() => handleViewHistory(record)}>记录</Button>
|
||
)
|
||
}
|
||
];
|
||
|
||
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: 'history', label: <span>执行记录 <Badge count={executionHistory.length} style={{ marginLeft: 8 }} /></span>, children: <Table columns={historyColumns} dataSource={executionHistory} rowKey="id" loading={loading} pagination={{ pageSize: 10 }} scroll={{ x: 1500 }} /> },
|
||
];
|
||
|
||
// 获取完整的申请详情
|
||
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',
|
||
headers: {
|
||
authorization: 'authorization-text',
|
||
},
|
||
onChange(info: any) {
|
||
// 更新文件列表状态
|
||
setVoucherFiles(info.fileList);
|
||
|
||
if (info.file.status === 'done') {
|
||
message.success(`${info.file.name} 上传成功`);
|
||
// 如果上传成功,将返回的URL添加到文件对象中
|
||
const updatedFileList = info.fileList.map((file: any) => {
|
||
if (file.uid === info.file.uid && file.response) {
|
||
return {
|
||
...file,
|
||
url: file.response.data?.url || file.response.url || file.response
|
||
};
|
||
}
|
||
return file;
|
||
});
|
||
setVoucherFiles(updatedFileList);
|
||
} else if (info.file.status === 'error') {
|
||
message.error(`${info.file.name} 上传失败`);
|
||
}
|
||
},
|
||
fileList: voucherFiles,
|
||
};
|
||
|
||
return (
|
||
<div style={{ padding: 24 }}>
|
||
<div style={{ marginBottom: 24 }}><h2 style={{ marginBottom: 8 }}>执行管理</h2><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 === 'approved' || 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={handleExecute}>执行</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 || fullDetail.advance_date || fullDetail.reimbursement_date || fullDetail.payment_date || fullDetail.verification_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="关联项目">项目ID: {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="关联项目">项目ID: {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>
|
||
|
||
{/* 明细清单 */}
|
||
{fullDetail.detail_items && fullDetail.detail_items.length > 0 && (
|
||
<>
|
||
<Divider>明细清单</Divider>
|
||
{renderDetailItems(fullDetail.detail_items)}
|
||
</>
|
||
)}
|
||
|
||
{/* 凭证附件 */}
|
||
{fullDetail.attachments && fullDetail.attachments.length > 0 && (
|
||
<>
|
||
<Divider>申请凭证附件</Divider>
|
||
{renderAttachments(fullDetail.attachments)}
|
||
</>
|
||
)}
|
||
|
||
{/* 执行表单 */}
|
||
{(selectedRecord.status === 'approved' || selectedRecord.status === 'pending') && (
|
||
<>
|
||
<Divider>执行信息</Divider>
|
||
<Form form={form} layout="vertical">
|
||
<Form.Item name="execute_date" label="执行日期" rules={[{ required: true }]}>
|
||
<DatePicker style={{ width: '100%' }} />
|
||
</Form.Item>
|
||
<Form.Item name="execute_method" label="执行方式" rules={[{ required: true }]}>
|
||
<Select options={[{ value: 'bank', label: '银行转账' }, { value: 'cash', label: '现金' }, { value: 'check', label: '支票' }, { value: 'other', label: '其他' }]} />
|
||
</Form.Item>
|
||
<Form.Item label="付款凭证" required>
|
||
<Upload {...uploadProps}>
|
||
<Button icon={<UploadOutlined />}>上传付款凭证</Button>
|
||
</Upload>
|
||
<div style={{ marginTop: 8, color: '#666', fontSize: 12 }}>
|
||
请上传付款凭证(银行转账回单、现金收据等),支持图片和PDF格式
|
||
</div>
|
||
</Form.Item>
|
||
<Form.Item name="remark" label="备注">
|
||
<TextArea rows={2} 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?.code}`} open={historyModalVisible} onCancel={() => setHistoryModalVisible(false)} footer={null} width={1000}>
|
||
<Table columns={historyColumns} dataSource={executionHistory.filter(r => r.applyCode === selectedRecord?.code)} rowKey="id" pagination={false} size="small" />
|
||
</Modal>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ExecutionManagement; |