493 lines
16 KiB
TypeScript
493 lines
16 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import {
|
|
Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card,
|
|
Row, Col, DatePicker, InputNumber, Descriptions, Divider
|
|
} from 'antd'
|
|
import {
|
|
PlusOutlined, EditOutlined, EyeOutlined, CheckOutlined,
|
|
CloseOutlined
|
|
} from '@ant-design/icons'
|
|
import type { ColumnsType } from 'antd/es/table'
|
|
import dayjs from 'dayjs'
|
|
import { useLanguageStore } from '../store/languageStore'
|
|
|
|
// ==================== 类型定义 ====================
|
|
interface PaymentPlan {
|
|
id: number
|
|
purchase_order_id: number
|
|
code: string
|
|
payment_date: string
|
|
amount: number
|
|
currency: string
|
|
payment_type: string
|
|
status: string
|
|
description: string
|
|
created_by: string
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
interface PurchaseOrder {
|
|
id: number
|
|
code: string
|
|
supplier_name: string
|
|
total_amount: number
|
|
currency: string
|
|
}
|
|
|
|
// ==================== 组件 ====================
|
|
const PaymentPlansPage: React.FC = () => {
|
|
const { t, currentLanguage } = useLanguageStore();
|
|
// 状态
|
|
const [paymentPlans, setPaymentPlans] = useState<PaymentPlan[]>([])
|
|
const [loading, setLoading] = useState(false)
|
|
const [purchaseOrders, setPurchaseOrders] = useState<PurchaseOrder[]>([])
|
|
|
|
// 弹窗状态
|
|
const [modalVisible, setModalVisible] = useState(false)
|
|
const [detailModalVisible, setDetailModalVisible] = useState(false)
|
|
const [editingPlan, setEditingPlan] = useState<PaymentPlan | null>(null)
|
|
const [viewingPlan, setViewingPlan] = useState<PaymentPlan | null>(null)
|
|
|
|
// 表单
|
|
const [form] = Form.useForm()
|
|
const navigate = useNavigate()
|
|
|
|
// ==================== 数据加载 ====================
|
|
|
|
const fetchPaymentPlans = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const response = await fetch('/api/payment-plans')
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
setPaymentPlans(data.data)
|
|
} else {
|
|
message.error(t('paymentPlan.getListFailed'))
|
|
}
|
|
} catch (error) {
|
|
console.error('获取付款计划列表失败:', error)
|
|
message.error(t('paymentPlan.getListFailed'))
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const fetchPurchaseOrders = async () => {
|
|
try {
|
|
const response = await fetch('/api/purchase-orders')
|
|
const data = await response.json()
|
|
if (data.success) {
|
|
setPurchaseOrders(data.data)
|
|
}
|
|
} catch (error) {
|
|
console.error('获取采购订单列表失败:', error)
|
|
}
|
|
}
|
|
|
|
const fetchPlanDetail = async (id: number) => {
|
|
try {
|
|
const response = await fetch(`/api/payment-plans/${id}`)
|
|
const data = await response.json()
|
|
if (data.success) {
|
|
setViewingPlan(data.data)
|
|
setDetailModalVisible(true)
|
|
} else {
|
|
message.error(t('paymentPlan.getDetailFailed'))
|
|
}
|
|
} catch (error) {
|
|
console.error('获取付款计划详情失败:', error)
|
|
message.error(t('paymentPlan.getDetailFailed'))
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
fetchPurchaseOrders()
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
fetchPaymentPlans()
|
|
}, [])
|
|
|
|
// ==================== 操作函数 ====================
|
|
|
|
const handleCreate = () => {
|
|
setEditingPlan(null)
|
|
form.resetFields()
|
|
form.setFieldsValue({
|
|
payment_date: dayjs(),
|
|
currency: 'CNY',
|
|
payment_type: 'partial',
|
|
status: 'pending',
|
|
created_by: t('common.systemAdmin')
|
|
})
|
|
setModalVisible(true)
|
|
}
|
|
|
|
const handleEdit = async (record: PaymentPlan) => {
|
|
try {
|
|
// 获取完整的付款计划详情
|
|
const response = await fetch(`/api/payment-plans/${record.id}`)
|
|
const data = await response.json()
|
|
|
|
if (data.success && data.data) {
|
|
const fullRecord = data.data
|
|
setEditingPlan(fullRecord)
|
|
|
|
// 打开模态框
|
|
setModalVisible(true);
|
|
|
|
// 使用 setTimeout 确保模态框已渲染后再设置表单值
|
|
setTimeout(() => {
|
|
form.resetFields();
|
|
|
|
// 设置表单值
|
|
form.setFieldsValue({
|
|
purchase_order_id: fullRecord.purchase_order_id,
|
|
payment_date: fullRecord.payment_date ? dayjs(fullRecord.payment_date) : dayjs(),
|
|
amount: fullRecord.amount,
|
|
currency: fullRecord.currency || 'CNY',
|
|
payment_type: fullRecord.payment_type || 'partial',
|
|
status: fullRecord.status || 'pending',
|
|
description: fullRecord.description,
|
|
created_by: fullRecord.created_by || t('common.systemAdmin')
|
|
});
|
|
}, 100);
|
|
} else {
|
|
message.error(t('paymentPlan.getDetailFailed'))
|
|
}
|
|
} catch (error) {
|
|
console.error('获取付款计划详情失败:', error)
|
|
message.error(t('paymentPlan.getDetailFailed'))
|
|
}
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
const values = await form.validateFields()
|
|
|
|
const requestData = {
|
|
...values,
|
|
payment_date: values.payment_date.format('YYYY-MM-DD')
|
|
}
|
|
|
|
let response
|
|
if (editingPlan) {
|
|
// 更新现有付款计划
|
|
response = await fetch(`/api/payment-plans/${editingPlan.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestData)
|
|
})
|
|
} else {
|
|
// 创建新付款计划
|
|
response = await fetch('/api/payment-plans', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestData)
|
|
})
|
|
}
|
|
|
|
const data = await response.json()
|
|
|
|
if (data.success) {
|
|
message.success(editingPlan ? t('common.saveSuccess') : t('common.createSuccess'))
|
|
setModalVisible(false)
|
|
fetchPaymentPlans()
|
|
} else {
|
|
message.error(editingPlan ? t('common.saveFailed') : t('common.operationFailed'))
|
|
}
|
|
} catch (error) {
|
|
console.error('保存失败:', error)
|
|
message.error(t('common.saveFailed'))
|
|
}
|
|
}
|
|
|
|
// ==================== 渲染 ====================
|
|
|
|
const getStatusTag = (status: string) => {
|
|
const statusMap: Record<string, { color: string; text: string }> = {
|
|
pending: { color: 'blue', text: t('paymentPlan.pending') },
|
|
approved: { color: 'green', text: t('paymentPlan.approved') },
|
|
executed: { color: 'purple', text: t('paymentPlan.executed') },
|
|
cancelled: { color: 'red', text: t('paymentPlan.cancelled') }
|
|
}
|
|
const info = statusMap[status] || { color: 'default', text: status }
|
|
return <Tag color={info.color}>{info.text}</Tag>
|
|
}
|
|
|
|
const getPaymentTypeTag = (type: string) => {
|
|
const typeMap: Record<string, { color: string; text: string }> = {
|
|
partial: { color: 'blue', text: t('paymentPlan.partialPayment') },
|
|
full: { color: 'green', text: t('paymentPlan.fullPayment') }
|
|
}
|
|
const info = typeMap[type] || { color: 'default', text: type }
|
|
return <Tag color={info.color}>{info.text}</Tag>
|
|
}
|
|
|
|
const columns: ColumnsType<PaymentPlan> = [
|
|
{
|
|
title: t('paymentPlan.planCode'),
|
|
dataIndex: 'code',
|
|
key: 'code',
|
|
width: 150,
|
|
ellipsis: true
|
|
},
|
|
{
|
|
title: t('paymentPlan.purchaseOrder'),
|
|
dataIndex: 'purchase_order_id',
|
|
key: 'purchase_order_id',
|
|
width: 140,
|
|
render: (id) => {
|
|
const order = purchaseOrders.find(o => o.id == id)
|
|
return order ? order.code : id
|
|
}
|
|
},
|
|
{
|
|
title: t('paymentPlan.paymentDate'),
|
|
dataIndex: 'payment_date',
|
|
key: 'payment_date',
|
|
width: 110,
|
|
render: (date) => dayjs(date).format('MM-DD')
|
|
},
|
|
{
|
|
title: t('paymentPlan.amount'),
|
|
dataIndex: 'amount',
|
|
key: 'amount',
|
|
width: 120,
|
|
align: 'right',
|
|
render: (amount, record) => (
|
|
<span style={{ fontWeight: 500, color: '#1890ff' }}>
|
|
{record.currency} {amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
title: t('paymentPlan.paymentType'),
|
|
dataIndex: 'payment_type',
|
|
key: 'payment_type',
|
|
width: 100,
|
|
align: 'center',
|
|
render: getPaymentTypeTag
|
|
},
|
|
{
|
|
title: t('paymentPlan.status'),
|
|
dataIndex: 'status',
|
|
key: 'status',
|
|
width: 90,
|
|
align: 'center',
|
|
render: getStatusTag
|
|
},
|
|
{
|
|
title: t('paymentPlan.creator'),
|
|
dataIndex: 'created_by',
|
|
key: 'created_by',
|
|
width: 100
|
|
},
|
|
{
|
|
title: t('paymentPlan.action'),
|
|
key: 'actions',
|
|
width: 150,
|
|
fixed: 'right',
|
|
render: (_, record) => (
|
|
<Space size={4}>
|
|
<Button
|
|
size="small"
|
|
type="text"
|
|
icon={<EyeOutlined />}
|
|
onClick={() => fetchPlanDetail(record.id)}
|
|
>
|
|
{t('common.detail')}
|
|
</Button>
|
|
<Button
|
|
size="small"
|
|
type="primary"
|
|
icon={<EditOutlined />}
|
|
onClick={() => handleEdit(record)}
|
|
>
|
|
{t('common.edit')}
|
|
</Button>
|
|
</Space>
|
|
)
|
|
}
|
|
]
|
|
|
|
return (
|
|
<div style={{ padding: 24 }}>
|
|
<div style={{ marginBottom: 24 }}>
|
|
<h2 style={{ marginBottom: 8 }}>{t('paymentPlan.title')}</h2>
|
|
<p style={{ color: '#888', marginBottom: 0 }}>{t('paymentPlan.description')}</p>
|
|
</div>
|
|
|
|
<Card extra={<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>{t('paymentPlan.newPlan')}</Button>}>
|
|
<Table
|
|
columns={columns}
|
|
dataSource={paymentPlans}
|
|
rowKey="id"
|
|
loading={loading}
|
|
pagination={{ pageSize: 20 }}
|
|
size="small"
|
|
scroll={{ x: 1200 }}
|
|
/>
|
|
</Card>
|
|
|
|
{/* 编辑/新建弹窗 */}
|
|
<Modal
|
|
title={editingPlan ? t('paymentPlan.editPlan') : t('paymentPlan.newPlan')}
|
|
open={modalVisible}
|
|
onCancel={() => {
|
|
form.resetFields();
|
|
setModalVisible(false);
|
|
setEditingPlan(null);
|
|
}}
|
|
footer={[
|
|
<Button key="cancel" onClick={() => {
|
|
form.resetFields();
|
|
setModalVisible(false);
|
|
setEditingPlan(null);
|
|
}}>{t('common.cancel')}</Button>,
|
|
<Button key="save" type="primary" onClick={handleSave}>{t('common.save')}</Button>
|
|
]}
|
|
destroyOnClose
|
|
width={600}
|
|
>
|
|
<Form form={form} layout="vertical">
|
|
<Row gutter={16}>
|
|
<Col span={24}>
|
|
<Form.Item
|
|
name="purchase_order_id"
|
|
label={t('paymentPlan.purchaseOrder')}
|
|
rules={[{ required: true, message: t('paymentPlan.selectOrder') }]}
|
|
>
|
|
<Select placeholder={t('paymentPlan.selectOrder')} allowClear>
|
|
{purchaseOrders.map(order => (
|
|
<Select.Option key={order.id} value={order.id}>
|
|
{order.code} - {order.supplier_name} ({order.currency} {order.total_amount})
|
|
</Select.Option>
|
|
))}
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="payment_date"
|
|
label={t('paymentPlan.paymentDate')}
|
|
rules={[{ required: true, message: t('paymentPlan.selectDate') }]}
|
|
>
|
|
<DatePicker style={{ width: '100%' }} />
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="amount"
|
|
label={t('paymentPlan.amount')}
|
|
rules={[{ required: true, message: t('paymentPlan.inputAmount') }]}
|
|
>
|
|
<InputNumber min={0} precision={2} style={{ width: '100%' }} placeholder={t('paymentPlan.amountPlaceholder')} />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="currency"
|
|
label={t('common.currency')}
|
|
rules={[{ required: true, message: t('paymentPlan.selectCurrency') }]}
|
|
>
|
|
<Select placeholder={t('paymentPlan.selectCurrency')}>
|
|
<Select.Option value="CNY">{t('paymentPlan.currencyCNY')}</Select.Option>
|
|
<Select.Option value="USD">{t('paymentPlan.currencyUSD')}</Select.Option>
|
|
<Select.Option value="LAK">{t('paymentPlan.currencyLAK')}</Select.Option>
|
|
<Select.Option value="THB">{t('paymentPlan.currencyTHB')}</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="payment_type"
|
|
label={t('paymentPlan.paymentType')}
|
|
rules={[{ required: true, message: t('paymentPlan.selectType') }]}
|
|
>
|
|
<Select placeholder={t('paymentPlan.selectType')}>
|
|
<Select.Option value="partial">{t('paymentPlan.partialPayment')}</Select.Option>
|
|
<Select.Option value="full">{t('paymentPlan.fullPayment')}</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Row gutter={16}>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="status"
|
|
label={t('paymentPlan.status')}
|
|
rules={[{ required: true, message: t('paymentPlan.selectStatus') }]}
|
|
>
|
|
<Select placeholder={t('paymentPlan.selectStatus')}>
|
|
<Select.Option value="pending">{t('paymentPlan.pending')}</Select.Option>
|
|
<Select.Option value="approved">{t('paymentPlan.approved')}</Select.Option>
|
|
<Select.Option value="executed">{t('paymentPlan.executed')}</Select.Option>
|
|
<Select.Option value="cancelled">{t('paymentPlan.cancelled')}</Select.Option>
|
|
</Select>
|
|
</Form.Item>
|
|
</Col>
|
|
<Col span={12}>
|
|
<Form.Item
|
|
name="created_by"
|
|
label={t('paymentPlan.creator')}
|
|
rules={[{ required: true, message: t('paymentPlan.inputCreator') }]}
|
|
>
|
|
<Input placeholder={t('paymentPlan.inputCreator')} />
|
|
</Form.Item>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Form.Item
|
|
name="description"
|
|
label={t('common.remark')}
|
|
>
|
|
<Input.TextArea rows={3} placeholder={t('paymentPlan.descPlaceholder')} />
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
|
|
{/* 详情弹窗 */}
|
|
<Modal
|
|
title={t('paymentPlan.detailTitle')}
|
|
open={detailModalVisible}
|
|
onCancel={() => setDetailModalVisible(false)}
|
|
footer={null}
|
|
width={600}
|
|
>
|
|
{viewingPlan && (
|
|
<>
|
|
<Descriptions bordered column={2} size="small">
|
|
<Descriptions.Item label={t('paymentPlan.detailCode')}>{viewingPlan.code}</Descriptions.Item>
|
|
<Descriptions.Item label={t('paymentPlan.status')}>{getStatusTag(viewingPlan.status)}</Descriptions.Item>
|
|
<Descriptions.Item label={t('paymentPlan.purchaseOrder')}>
|
|
{(() => {
|
|
const order = purchaseOrders.find(o => o.id == viewingPlan.purchase_order_id)
|
|
return order ? order.code : viewingPlan.purchase_order_id
|
|
})()}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label={t('paymentPlan.paymentType')}>{getPaymentTypeTag(viewingPlan.payment_type)}</Descriptions.Item>
|
|
<Descriptions.Item label={t('paymentPlan.paymentDate')}>{viewingPlan.payment_date}</Descriptions.Item>
|
|
<Descriptions.Item label={t('common.currency')}>{viewingPlan.currency}</Descriptions.Item>
|
|
<Descriptions.Item label={t('paymentPlan.amount')} span={2}>{viewingPlan.amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</Descriptions.Item>
|
|
<Descriptions.Item label={t('common.remark')} span={2}>{viewingPlan.description || '-'}</Descriptions.Item>
|
|
<Descriptions.Item label={t('paymentPlan.creator')} span={2}>{viewingPlan.created_by}</Descriptions.Item>
|
|
</Descriptions>
|
|
</>
|
|
)}
|
|
</Modal>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default PaymentPlansPage; |