925 lines
31 KiB
TypeScript
925 lines
31 KiB
TypeScript
/**
|
|||
|
|
* 采购申请页面
|
||
|
|
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
|
||
|
|
* 章节:二、采购申请页面改造
|
||
|
|
*
|
||
|
|
* 简化后的采购申请表单:
|
||
|
|
* - 不再录入供应商(询价前未知)
|
||
|
|
* - 不再录入商品明细(询价后确定)
|
||
|
|
* - 仅填写需求描述和预计金额
|
||
|
|
* - 新增需求日期字段
|
||
|
|
*/
|
||
|
|
import React, { useState, useEffect } from 'react'
|
||
|
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||
|
|
import {
|
||
|
|
Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card,
|
||
|
|
Row, Col, DatePicker, InputNumber, Popconfirm, Tabs, Empty, Spin, Descriptions, Upload
|
||
|
|
} from 'antd'
|
||
|
|
import {
|
||
|
|
PlusOutlined, EditOutlined, DeleteOutlined,
|
||
|
|
CheckOutlined, CloseOutlined, EyeOutlined, UndoOutlined, UploadOutlined
|
||
|
|
} from '@ant-design/icons'
|
||
|
|
import type { ColumnsType } from 'antd/es/table'
|
||
|
|
import dayjs from 'dayjs'
|
||
|
|
|
||
|
|
interface PurchaseRequest {
|
||
|
|
id: number
|
||
|
|
code: string
|
||
|
|
request_code: string
|
||
|
|
project_id: number
|
||
|
|
project_name?: string
|
||
|
|
applicant: string
|
||
|
|
request_date: string
|
||
|
|
expense_category: string
|
||
|
|
total_amount: number
|
||
|
|
currency: string
|
||
|
|
status: string
|
||
|
|
remark?: string
|
||
|
|
attachments?: string
|
||
|
|
purchase_type: string
|
||
|
|
brief_description: string
|
||
|
|
expected_date: string
|
||
|
|
created_at: string
|
||
|
|
updated_at: string
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Project {
|
||
|
|
id: number
|
||
|
|
name: string
|
||
|
|
}
|
||
|
|
|
||
|
|
const PurchaseRequestsPage: React.FC = () => {
|
||
|
|
const [purchaseRequests, setPurchaseRequests] = useState<PurchaseRequest[]>([])
|
||
|
|
const [completedRequests, setCompletedRequests] = useState<PurchaseRequest[]>([])
|
||
|
|
const [activeTab, setActiveTab] = useState('active')
|
||
|
|
const [loading, setLoading] = useState(false)
|
||
|
|
const [projects, setProjects] = useState<Project[]>([])
|
||
|
|
|
||
|
|
const [selectedProjectId, setSelectedProjectId] = useState<number | null>(null)
|
||
|
|
const [selectedStatus, setSelectedStatus] = useState<string | null>(null)
|
||
|
|
|
||
|
|
const [modalVisible, setModalVisible] = useState(false)
|
||
|
|
const [detailModalVisible, setDetailModalVisible] = useState(false)
|
||
|
|
const [editingRequest, setEditingRequest] = useState<PurchaseRequest | null>(null)
|
||
|
|
const [viewingRequest, setViewingRequest] = useState<PurchaseRequest | null>(null)
|
||
|
|
const [currentEditingStatus, setCurrentEditingStatus] = useState<string>('')
|
||
|
|
|
||
|
|
const [purchaseType, setPurchaseType] = useState<'inventory' | 'project'>('inventory')
|
||
|
|
const [currency, setCurrency] = useState<string>('CNY')
|
||
|
|
const [attachments, setAttachments] = useState<any[]>([])
|
||
|
|
|
||
|
|
const [form] = Form.useForm()
|
||
|
|
const navigate = useNavigate()
|
||
|
|
const location = useLocation()
|
||
|
|
|
||
|
|
const exchangeRates = {
|
||
|
|
CNY: 1,
|
||
|
|
USD: 7.2,
|
||
|
|
LAK: 0.0004,
|
||
|
|
THB: 0.2
|
||
|
|
}
|
||
|
|
|
||
|
|
const fetchPurchaseRequests = async () => {
|
||
|
|
setLoading(true)
|
||
|
|
try {
|
||
|
|
const params = new URLSearchParams()
|
||
|
|
if (selectedProjectId) params.append('project_id', selectedProjectId.toString())
|
||
|
|
if (selectedStatus) params.append('status', selectedStatus)
|
||
|
|
|
||
|
|
const response = await fetch(`/api/purchase-requests?${params}`)
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
const active = data.data.filter((item: PurchaseRequest) =>
|
||
|
|
['pending_edit', 'pending', 'withdrawn'].includes(item.status))
|
||
|
|
const completed = data.data.filter((item: PurchaseRequest) =>
|
||
|
|
['approved', 'executed'].includes(item.status))
|
||
|
|
setPurchaseRequests(active)
|
||
|
|
setCompletedRequests(completed)
|
||
|
|
} else {
|
||
|
|
message.error('获取采购申请列表失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取采购申请列表失败:', error)
|
||
|
|
message.error('获取采购申请列表失败')
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const fetchProjects = async () => {
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/projects')
|
||
|
|
const data = await response.json()
|
||
|
|
if (data.success) {
|
||
|
|
setProjects(data.data)
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取项目列表失败:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const fetchRequestDetail = async (id: number) => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/purchase-requests/${id}`)
|
||
|
|
const data = await response.json()
|
||
|
|
if (data.success) {
|
||
|
|
setViewingRequest(data.data)
|
||
|
|
setDetailModalVisible(true)
|
||
|
|
} else {
|
||
|
|
message.error('获取采购申请详情失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取采购申请详情失败:', error)
|
||
|
|
message.error('获取采购申请详情失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchProjects()
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchPurchaseRequests()
|
||
|
|
}, [selectedProjectId, selectedStatus])
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
const state = location.state as { formValues?: any, fromPurchaseRequest?: boolean }
|
||
|
|
let formValues = state?.formValues
|
||
|
|
|
||
|
|
if (!formValues) {
|
||
|
|
const storedValues = sessionStorage.getItem('purchaseRequestFormValues')
|
||
|
|
if (storedValues) {
|
||
|
|
formValues = JSON.parse(storedValues)
|
||
|
|
sessionStorage.removeItem('purchaseRequestFormValues')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (formValues || state?.fromPurchaseRequest) {
|
||
|
|
setTimeout(() => {
|
||
|
|
if (formValues) {
|
||
|
|
const values = {
|
||
|
|
...formValues,
|
||
|
|
request_date: formValues.request_date ? dayjs(formValues.request_date) : undefined,
|
||
|
|
expected_date: formValues.expected_date ? dayjs(formValues.expected_date) : undefined
|
||
|
|
}
|
||
|
|
form.setFieldsValue(values)
|
||
|
|
}
|
||
|
|
setModalVisible(true)
|
||
|
|
}, 100)
|
||
|
|
}
|
||
|
|
}, [location.state, form])
|
||
|
|
|
||
|
|
const handleCreate = () => {
|
||
|
|
setEditingRequest(null)
|
||
|
|
setPurchaseType('inventory')
|
||
|
|
form.resetFields()
|
||
|
|
form.setFieldsValue({
|
||
|
|
purchase_type: 'inventory',
|
||
|
|
request_date: dayjs(),
|
||
|
|
expected_date: dayjs().add(7, 'day'),
|
||
|
|
currency: 'CNY',
|
||
|
|
expense_category: 'material',
|
||
|
|
applicant: '系统管理员',
|
||
|
|
total_amount: 0,
|
||
|
|
attachments: []
|
||
|
|
})
|
||
|
|
setAttachments([])
|
||
|
|
setCurrentEditingStatus('')
|
||
|
|
setModalVisible(true)
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleEdit = async (record: PurchaseRequest) => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/purchase-requests/${record.id}`)
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (data.success && data.data) {
|
||
|
|
const fullRecord = data.data
|
||
|
|
setEditingRequest(fullRecord)
|
||
|
|
setPurchaseType(fullRecord.purchase_type as 'inventory' | 'project')
|
||
|
|
setCurrentEditingStatus(fullRecord.status)
|
||
|
|
|
||
|
|
let attachmentsArray: any[] = []
|
||
|
|
if (fullRecord.attachments) {
|
||
|
|
if (typeof fullRecord.attachments === 'string') {
|
||
|
|
attachmentsArray = fullRecord.attachments.split(',').map((url: string) => ({
|
||
|
|
url: url,
|
||
|
|
name: url.split('/').pop() || '',
|
||
|
|
uid: url,
|
||
|
|
status: 'done'
|
||
|
|
}))
|
||
|
|
} else if (Array.isArray(fullRecord.attachments)) {
|
||
|
|
attachmentsArray = fullRecord.attachments
|
||
|
|
}
|
||
|
|
}
|
||
|
|
setAttachments(attachmentsArray)
|
||
|
|
|
||
|
|
setModalVisible(true)
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
form.resetFields()
|
||
|
|
form.setFieldsValue({
|
||
|
|
purchase_type: fullRecord.purchase_type || 'inventory',
|
||
|
|
project_id: fullRecord.project_id,
|
||
|
|
request_date: fullRecord.request_date ? dayjs(fullRecord.request_date) : dayjs(),
|
||
|
|
expected_date: fullRecord.expected_date ? dayjs(fullRecord.expected_date) : undefined,
|
||
|
|
applicant: fullRecord.applicant,
|
||
|
|
brief_description: fullRecord.brief_description,
|
||
|
|
remark: fullRecord.remark,
|
||
|
|
expense_category: fullRecord.expense_category || 'material',
|
||
|
|
currency: fullRecord.currency || 'CNY',
|
||
|
|
total_amount: fullRecord.total_amount || 0,
|
||
|
|
attachments: attachmentsArray
|
||
|
|
})
|
||
|
|
}, 100)
|
||
|
|
} else {
|
||
|
|
message.error('获取采购申请详情失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('获取采购申请详情失败:', error)
|
||
|
|
message.error('获取采购申请详情失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleDelete = async (id: number) => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/purchase-requests/${id}`, { method: 'DELETE' })
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
message.success('删除成功')
|
||
|
|
fetchPurchaseRequests()
|
||
|
|
} else {
|
||
|
|
message.error('删除失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('删除失败:', error)
|
||
|
|
message.error('删除失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleSave = async () => {
|
||
|
|
try {
|
||
|
|
const values = await form.validateFields()
|
||
|
|
const saveStatus = currentEditingStatus || 'pending_edit'
|
||
|
|
|
||
|
|
const attachmentsUrl = attachments && attachments.length > 0
|
||
|
|
? attachments.map((file: any) => file.url).join(',')
|
||
|
|
: ''
|
||
|
|
|
||
|
|
const requestData = {
|
||
|
|
...values,
|
||
|
|
request_date: values.request_date.format('YYYY-MM-DD'),
|
||
|
|
expected_date: values.expected_date ? values.expected_date.format('YYYY-MM-DD') : null,
|
||
|
|
applicant: '系统管理员',
|
||
|
|
status: saveStatus,
|
||
|
|
attachments: attachmentsUrl
|
||
|
|
}
|
||
|
|
|
||
|
|
let response
|
||
|
|
if (editingRequest) {
|
||
|
|
response = await fetch(`/api/purchase-requests/${editingRequest.id}`, {
|
||
|
|
method: 'PUT',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(requestData)
|
||
|
|
})
|
||
|
|
} else {
|
||
|
|
response = await fetch('/api/purchase-requests', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(requestData)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
message.success(editingRequest ? '保存成功' : '创建成功')
|
||
|
|
if (!editingRequest) {
|
||
|
|
setEditingRequest(data.data)
|
||
|
|
}
|
||
|
|
setSelectedStatus(null)
|
||
|
|
fetchPurchaseRequests()
|
||
|
|
setModalVisible(false)
|
||
|
|
} else {
|
||
|
|
message.error(editingRequest ? '保存失败' : '创建失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('保存失败:', error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleFormSubmit = async () => {
|
||
|
|
try {
|
||
|
|
const values = await form.validateFields()
|
||
|
|
|
||
|
|
const attachmentsUrl = attachments && attachments.length > 0
|
||
|
|
? attachments.map((file: any) => file.url).join(',')
|
||
|
|
: ''
|
||
|
|
|
||
|
|
const requestData = {
|
||
|
|
...values,
|
||
|
|
request_date: values.request_date.format('YYYY-MM-DD'),
|
||
|
|
expected_date: values.expected_date ? values.expected_date.format('YYYY-MM-DD') : null,
|
||
|
|
applicant: '系统管理员',
|
||
|
|
status: 'pending_edit',
|
||
|
|
attachments: attachmentsUrl
|
||
|
|
}
|
||
|
|
|
||
|
|
let response
|
||
|
|
let purchaseRequestId: number
|
||
|
|
let data
|
||
|
|
|
||
|
|
if (editingRequest) {
|
||
|
|
response = await fetch(`/api/purchase-requests/${editingRequest.id}`, {
|
||
|
|
method: 'PUT',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(requestData)
|
||
|
|
})
|
||
|
|
data = await response.json()
|
||
|
|
purchaseRequestId = editingRequest.id
|
||
|
|
} else {
|
||
|
|
response = await fetch('/api/purchase-requests', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(requestData)
|
||
|
|
})
|
||
|
|
data = await response.json()
|
||
|
|
if (data.success) {
|
||
|
|
purchaseRequestId = data.data.id
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (data.success && purchaseRequestId) {
|
||
|
|
const submitResponse = await fetch(`/api/purchase-requests/${purchaseRequestId}/submit`, {
|
||
|
|
method: 'POST'
|
||
|
|
})
|
||
|
|
const submitData = await submitResponse.json()
|
||
|
|
|
||
|
|
if (submitData.success) {
|
||
|
|
message.success(editingRequest ? '提交成功' : '创建并提交成功')
|
||
|
|
setModalVisible(false)
|
||
|
|
setSelectedStatus(null)
|
||
|
|
fetchPurchaseRequests()
|
||
|
|
} else {
|
||
|
|
message.error('提交失败')
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
message.error(editingRequest ? '保存失败' : '创建失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('提交失败:', error)
|
||
|
|
message.error('提交失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleWithdraw = async (id: number) => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/purchase-requests/${id}/withdraw`, { method: 'POST' })
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
message.success('撤回成功')
|
||
|
|
setSelectedStatus(null)
|
||
|
|
fetchPurchaseRequests()
|
||
|
|
} else {
|
||
|
|
message.error('撤回失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('撤回失败:', error)
|
||
|
|
message.error('撤回失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleApprove = async (id: number) => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/purchase-requests/${id}/approve`, { method: 'POST' })
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
message.success(data.message || '审批通过成功')
|
||
|
|
setSelectedStatus(null)
|
||
|
|
fetchPurchaseRequests()
|
||
|
|
} else {
|
||
|
|
message.error('审批通过失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('审批通过失败:', error)
|
||
|
|
message.error('审批通过失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleReject = async (id: number) => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/purchase-requests/${id}/reject`, { method: 'POST' })
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (data.success) {
|
||
|
|
message.success('驳回成功')
|
||
|
|
setSelectedStatus(null)
|
||
|
|
fetchPurchaseRequests()
|
||
|
|
} else {
|
||
|
|
message.error('驳回失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('驳回失败:', error)
|
||
|
|
message.error('驳回失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const getStatusTag = (status: string) => {
|
||
|
|
const statusMap: Record<string, { color: string; text: string }> = {
|
||
|
|
pending_edit: { color: 'default', text: '待编辑' },
|
||
|
|
pending: { color: 'blue', text: '待审批' },
|
||
|
|
approved: { color: 'green', text: '已审批' },
|
||
|
|
executed: { color: 'purple', text: '已执行' },
|
||
|
|
withdrawn: { color: 'orange', text: '已撤回' }
|
||
|
|
}
|
||
|
|
const info = statusMap[status] || { color: 'default', text: status }
|
||
|
|
return <Tag color={info.color}>{info.text}</Tag>
|
||
|
|
}
|
||
|
|
|
||
|
|
const columns: ColumnsType<PurchaseRequest> = [
|
||
|
|
{
|
||
|
|
title: '事由',
|
||
|
|
dataIndex: 'brief_description',
|
||
|
|
key: 'brief_description',
|
||
|
|
width: 180,
|
||
|
|
ellipsis: true,
|
||
|
|
render: (v: string, r: PurchaseRequest) => (
|
||
|
|
<a onClick={() => fetchRequestDetail(r.id)} style={{ fontWeight: 500 }}>{v || '-'}</a>
|
||
|
|
)
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '项目',
|
||
|
|
dataIndex: 'project_name',
|
||
|
|
key: 'project_name',
|
||
|
|
width: 120,
|
||
|
|
ellipsis: true,
|
||
|
|
render: (v: string) => v || '-'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '分类',
|
||
|
|
dataIndex: 'expense_category',
|
||
|
|
key: 'expense_category',
|
||
|
|
width: 80,
|
||
|
|
render: (category) => {
|
||
|
|
const categoryMap: Record<string, string> = {
|
||
|
|
material: '材料',
|
||
|
|
equipment: '设备',
|
||
|
|
pole: '电杆',
|
||
|
|
other: '其他'
|
||
|
|
}
|
||
|
|
return <Tag size="small">{categoryMap[category] || category}</Tag>
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '预计金额',
|
||
|
|
dataIndex: 'total_amount',
|
||
|
|
key: 'total_amount',
|
||
|
|
width: 130,
|
||
|
|
align: 'right',
|
||
|
|
render: (amount, record) => (
|
||
|
|
<span style={{ fontWeight: 500, color: '#999' }}>
|
||
|
|
{record.currency} {amount?.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0.00'}
|
||
|
|
</span>
|
||
|
|
)
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '需求日期',
|
||
|
|
dataIndex: 'expected_date',
|
||
|
|
key: 'expected_date',
|
||
|
|
width: 100,
|
||
|
|
render: (date) => date ? dayjs(date).format('MM-DD') : '-'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '状态',
|
||
|
|
dataIndex: 'status',
|
||
|
|
key: 'status',
|
||
|
|
width: 90,
|
||
|
|
align: 'center',
|
||
|
|
render: getStatusTag
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '申请日期',
|
||
|
|
dataIndex: 'request_date',
|
||
|
|
key: 'request_date',
|
||
|
|
width: 100,
|
||
|
|
render: (date) => date ? dayjs(date).format('MM-DD') : '-'
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '申请人',
|
||
|
|
dataIndex: 'applicant',
|
||
|
|
key: 'applicant',
|
||
|
|
width: 90
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '编号',
|
||
|
|
dataIndex: 'request_code',
|
||
|
|
key: 'request_code',
|
||
|
|
width: 150,
|
||
|
|
ellipsis: true,
|
||
|
|
render: (v: string) => <span style={{ fontSize: 12, color: '#999' }}>{v || '-'}</span>
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: '操作',
|
||
|
|
key: 'actions',
|
||
|
|
width: 200,
|
||
|
|
fixed: 'right',
|
||
|
|
render: (_, record) => (
|
||
|
|
<Space size={4}>
|
||
|
|
<Button
|
||
|
|
size="small"
|
||
|
|
type="text"
|
||
|
|
icon={<EyeOutlined />}
|
||
|
|
onClick={() => fetchRequestDetail(record.id)}
|
||
|
|
/>
|
||
|
|
|
||
|
|
{record.status === 'pending' && (
|
||
|
|
<>
|
||
|
|
<Button
|
||
|
|
size="small"
|
||
|
|
type="text"
|
||
|
|
icon={<CheckOutlined />}
|
||
|
|
onClick={() => handleApprove(record.id)}
|
||
|
|
style={{ color: '#52c41a' }}
|
||
|
|
>
|
||
|
|
通过
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
size="small"
|
||
|
|
type="text"
|
||
|
|
icon={<CloseOutlined />}
|
||
|
|
onClick={() => handleReject(record.id)}
|
||
|
|
danger
|
||
|
|
>
|
||
|
|
驳回
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
size="small"
|
||
|
|
type="text"
|
||
|
|
icon={<UndoOutlined />}
|
||
|
|
onClick={() => handleWithdraw(record.id)}
|
||
|
|
>
|
||
|
|
撤回
|
||
|
|
</Button>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{(record.status === 'withdrawn' || record.status === 'pending_edit') && (
|
||
|
|
<>
|
||
|
|
<Button
|
||
|
|
size="small"
|
||
|
|
type="primary"
|
||
|
|
icon={<EditOutlined />}
|
||
|
|
onClick={() => handleEdit(record)}
|
||
|
|
>
|
||
|
|
编辑
|
||
|
|
</Button>
|
||
|
|
<Popconfirm
|
||
|
|
title="确定要删除吗?"
|
||
|
|
onConfirm={() => handleDelete(record.id)}
|
||
|
|
>
|
||
|
|
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
|
||
|
|
</Popconfirm>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</Space>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ padding: 24 }}>
|
||
|
|
<div style={{ marginBottom: 24 }}>
|
||
|
|
<h2 style={{ marginBottom: 8 }}>采购申请</h2>
|
||
|
|
<p style={{ color: '#888', marginBottom: 0 }}>管理公司采购申请(简化版:仅填写需求描述和预计金额)</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Card extra={<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>新建采购申请</Button>}>
|
||
|
|
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||
|
|
<Tabs.TabPane tab="活跃申请" key="active">
|
||
|
|
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||
|
|
<Col span={6}>
|
||
|
|
<Select
|
||
|
|
placeholder="选择项目筛选"
|
||
|
|
allowClear
|
||
|
|
style={{ width: '100%' }}
|
||
|
|
onChange={(value) => setSelectedProjectId(value)}
|
||
|
|
>
|
||
|
|
{projects.map(project => (
|
||
|
|
<Select.Option key={project.id} value={project.id}>
|
||
|
|
{project.name}
|
||
|
|
</Select.Option>
|
||
|
|
))}
|
||
|
|
</Select>
|
||
|
|
</Col>
|
||
|
|
<Col span={6}>
|
||
|
|
<Select
|
||
|
|
placeholder="选择状态筛选"
|
||
|
|
allowClear
|
||
|
|
style={{ width: '100%' }}
|
||
|
|
onChange={(value) => setSelectedStatus(value)}
|
||
|
|
>
|
||
|
|
<Select.Option value="pending">待审批</Select.Option>
|
||
|
|
<Select.Option value="withdrawn">已撤回</Select.Option>
|
||
|
|
<Select.Option value="pending_edit">待编辑</Select.Option>
|
||
|
|
</Select>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
<Table
|
||
|
|
columns={columns}
|
||
|
|
dataSource={purchaseRequests}
|
||
|
|
rowKey="id"
|
||
|
|
loading={loading}
|
||
|
|
pagination={{ pageSize: 20 }}
|
||
|
|
size="small"
|
||
|
|
scroll={{ x: 1200 }}
|
||
|
|
/>
|
||
|
|
</Tabs.TabPane>
|
||
|
|
<Tabs.TabPane tab="已完成" key="completed">
|
||
|
|
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||
|
|
<Col span={6}>
|
||
|
|
<Select
|
||
|
|
placeholder="选择项目筛选"
|
||
|
|
allowClear
|
||
|
|
style={{ width: '100%' }}
|
||
|
|
onChange={(value) => setSelectedProjectId(value)}
|
||
|
|
>
|
||
|
|
{projects.map(project => (
|
||
|
|
<Select.Option key={project.id} value={project.id}>
|
||
|
|
{project.name}
|
||
|
|
</Select.Option>
|
||
|
|
))}
|
||
|
|
</Select>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
<Table
|
||
|
|
columns={columns}
|
||
|
|
dataSource={completedRequests}
|
||
|
|
rowKey="id"
|
||
|
|
loading={loading}
|
||
|
|
pagination={{ pageSize: 20 }}
|
||
|
|
size="small"
|
||
|
|
scroll={{ x: 1200 }}
|
||
|
|
/>
|
||
|
|
</Tabs.TabPane>
|
||
|
|
</Tabs>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* 编辑/新建弹窗 - 简化版 */}
|
||
|
|
<Modal
|
||
|
|
title={editingRequest ? '编辑采购申请' : '新建采购申请'}
|
||
|
|
open={modalVisible}
|
||
|
|
onCancel={() => setModalVisible(false)}
|
||
|
|
footer={[
|
||
|
|
<Button key="cancel" onClick={() => setModalVisible(false)}>取消</Button>,
|
||
|
|
<Button key="save" onClick={handleSave}>保存</Button>,
|
||
|
|
<Button key="submit" type="primary" onClick={handleFormSubmit}>提交审批</Button>
|
||
|
|
]}
|
||
|
|
width={700}
|
||
|
|
>
|
||
|
|
<Form form={form} layout="vertical">
|
||
|
|
<Row gutter={16}>
|
||
|
|
<Col span={12}>
|
||
|
|
<Form.Item
|
||
|
|
name="purchase_type"
|
||
|
|
label="采购类型"
|
||
|
|
rules={[{ required: true, message: '请选择采购类型' }]}
|
||
|
|
>
|
||
|
|
<Select
|
||
|
|
placeholder="请选择采购类型"
|
||
|
|
onChange={(value) => setPurchaseType(value as 'inventory' | 'project')}
|
||
|
|
>
|
||
|
|
<Select.Option value="inventory">库存采购</Select.Option>
|
||
|
|
<Select.Option value="project">项目采购</Select.Option>
|
||
|
|
</Select>
|
||
|
|
</Form.Item>
|
||
|
|
</Col>
|
||
|
|
<Col span={12}>
|
||
|
|
{purchaseType === 'project' && (
|
||
|
|
<Form.Item
|
||
|
|
name="project_id"
|
||
|
|
label="关联项目"
|
||
|
|
rules={[{ required: true, message: '项目采购必须关联项目' }]}
|
||
|
|
>
|
||
|
|
<Select placeholder="请选择项目" allowClear>
|
||
|
|
{projects.map(project => (
|
||
|
|
<Select.Option key={project.id} value={project.id}>
|
||
|
|
{project.name}
|
||
|
|
</Select.Option>
|
||
|
|
))}
|
||
|
|
</Select>
|
||
|
|
</Form.Item>
|
||
|
|
)}
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
|
||
|
|
<Row gutter={16}>
|
||
|
|
<Col span={12}>
|
||
|
|
<Form.Item
|
||
|
|
name="applicant"
|
||
|
|
label="申请人"
|
||
|
|
initialValue="系统管理员"
|
||
|
|
rules={[{ required: true, message: '请输入申请人' }]}
|
||
|
|
>
|
||
|
|
<Input placeholder="请输入申请人" disabled />
|
||
|
|
</Form.Item>
|
||
|
|
</Col>
|
||
|
|
<Col span={12}>
|
||
|
|
<Form.Item
|
||
|
|
name="request_date"
|
||
|
|
label="申请日期"
|
||
|
|
rules={[{ required: true, message: '请选择申请日期' }]}
|
||
|
|
>
|
||
|
|
<DatePicker style={{ width: '100%' }} />
|
||
|
|
</Form.Item>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
|
||
|
|
<Form.Item
|
||
|
|
name="brief_description"
|
||
|
|
label="事由描述"
|
||
|
|
rules={[
|
||
|
|
{ required: true, message: '请输入事由描述' },
|
||
|
|
{ max: 100, message: '事由描述不能超过100个字符' }
|
||
|
|
]}
|
||
|
|
>
|
||
|
|
<Input.TextArea
|
||
|
|
rows={2}
|
||
|
|
placeholder="请简要描述采购需求(如:采购XX项目所需电缆、电杆等材料)"
|
||
|
|
maxLength={100}
|
||
|
|
showCount
|
||
|
|
/>
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
<Row gutter={16}>
|
||
|
|
<Col span={8}>
|
||
|
|
<Form.Item
|
||
|
|
name="expense_category"
|
||
|
|
label="支出分类"
|
||
|
|
rules={[{ required: true, message: '请选择支出分类' }]}
|
||
|
|
>
|
||
|
|
<Select placeholder="请选择支出分类">
|
||
|
|
<Select.Option value="material">材料</Select.Option>
|
||
|
|
<Select.Option value="equipment">设备</Select.Option>
|
||
|
|
<Select.Option value="pole">电杆</Select.Option>
|
||
|
|
<Select.Option value="other">其他</Select.Option>
|
||
|
|
</Select>
|
||
|
|
</Form.Item>
|
||
|
|
</Col>
|
||
|
|
<Col span={8}>
|
||
|
|
<Form.Item
|
||
|
|
name="total_amount"
|
||
|
|
label="预计金额"
|
||
|
|
rules={[{ required: true, message: '请输入预计金额' }]}
|
||
|
|
>
|
||
|
|
<InputNumber
|
||
|
|
style={{ width: '100%' }}
|
||
|
|
placeholder="预计金额"
|
||
|
|
min={0}
|
||
|
|
precision={2}
|
||
|
|
/>
|
||
|
|
</Form.Item>
|
||
|
|
</Col>
|
||
|
|
<Col span={8}>
|
||
|
|
<Form.Item
|
||
|
|
name="currency"
|
||
|
|
label="币种"
|
||
|
|
rules={[{ required: true, message: '请选择币种' }]}
|
||
|
|
>
|
||
|
|
<Select placeholder="请选择币种" onChange={(value) => setCurrency(value)}>
|
||
|
|
<Select.Option value="CNY">人民币</Select.Option>
|
||
|
|
<Select.Option value="USD">美元</Select.Option>
|
||
|
|
<Select.Option value="LAK">老挝基普</Select.Option>
|
||
|
|
<Select.Option value="THB">泰铢</Select.Option>
|
||
|
|
</Select>
|
||
|
|
</Form.Item>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
|
||
|
|
<Row gutter={16}>
|
||
|
|
<Col span={12}>
|
||
|
|
<Form.Item
|
||
|
|
name="expected_date"
|
||
|
|
label="需求日期"
|
||
|
|
rules={[{ required: true, message: '请选择需求日期' }]}
|
||
|
|
>
|
||
|
|
<DatePicker style={{ width: '100%' }} placeholder="期望到货日期" />
|
||
|
|
</Form.Item>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
|
||
|
|
<Form.Item name="remark" label="备注">
|
||
|
|
<Input.TextArea rows={2} placeholder="请输入备注(选填)" />
|
||
|
|
</Form.Item>
|
||
|
|
|
||
|
|
<Form.Item name="attachments" label="附件">
|
||
|
|
<Upload
|
||
|
|
name="file"
|
||
|
|
listType="text"
|
||
|
|
maxCount={5}
|
||
|
|
accept=".pdf,.doc,.docx,.xlsx,.xls,.jpg,.jpeg,.png"
|
||
|
|
fileList={attachments}
|
||
|
|
onChange={(info) => {
|
||
|
|
if (info.file.status === 'removed') {
|
||
|
|
const updatedAttachments = attachments.filter(item => item.uid !== info.file.uid)
|
||
|
|
setAttachments(updatedAttachments)
|
||
|
|
form.setFieldsValue({ attachments: updatedAttachments })
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
customRequest={async (options) => {
|
||
|
|
const { onSuccess, onError, file } = options
|
||
|
|
const formData = new FormData()
|
||
|
|
formData.append('file', file as File)
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/upload/single', {
|
||
|
|
method: 'POST',
|
||
|
|
body: formData
|
||
|
|
})
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (data.success && data.data) {
|
||
|
|
const fileInfo = {
|
||
|
|
...data.data,
|
||
|
|
name: (file as File).name,
|
||
|
|
uid: (file as any).uid,
|
||
|
|
status: 'done'
|
||
|
|
}
|
||
|
|
const updatedAttachments = [...attachments, fileInfo]
|
||
|
|
setAttachments(updatedAttachments)
|
||
|
|
form.setFieldsValue({ attachments: updatedAttachments })
|
||
|
|
onSuccess(fileInfo)
|
||
|
|
} else {
|
||
|
|
onError(new Error('上传失败'))
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
onError(error)
|
||
|
|
}
|
||
|
|
}}
|
||
|
|
onPreview={(file) => {
|
||
|
|
window.open(file.url, '_blank')
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||
|
|
</Upload>
|
||
|
|
</Form.Item>
|
||
|
|
</Form>
|
||
|
|
</Modal>
|
||
|
|
|
||
|
|
{/* 详情弹窗 */}
|
||
|
|
<Modal
|
||
|
|
title="采购申请详情"
|
||
|
|
open={detailModalVisible}
|
||
|
|
onCancel={() => setDetailModalVisible(false)}
|
||
|
|
footer={[<Button key="close" onClick={() => setDetailModalVisible(false)}>关闭</Button>]}
|
||
|
|
width={700}
|
||
|
|
>
|
||
|
|
{viewingRequest && (
|
||
|
|
<div>
|
||
|
|
<Card style={{ marginBottom: 16 }}>
|
||
|
|
<Descriptions bordered column={2}>
|
||
|
|
<Descriptions.Item label="申请编号" span={1}>{viewingRequest.request_code || viewingRequest.code}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="状态" span={1}>{getStatusTag(viewingRequest.status)}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="采购类型" span={1}>
|
||
|
|
{viewingRequest.purchase_type === 'project' ? '项目采购' : '库存采购'}
|
||
|
|
</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="项目" span={1}>{viewingRequest.project_name || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="申请人" span={1}>{viewingRequest.applicant}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="申请日期" span={1}>{viewingRequest.request_date}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="事由描述" span={2}>{viewingRequest.brief_description || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="支出分类" span={1}>
|
||
|
|
{{
|
||
|
|
material: '材料',
|
||
|
|
equipment: '设备',
|
||
|
|
pole: '电杆',
|
||
|
|
other: '其他'
|
||
|
|
}[viewingRequest.expense_category] || viewingRequest.expense_category}
|
||
|
|
</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="预计金额" span={1}>
|
||
|
|
<strong style={{ fontSize: 16, color: '#999' }}>
|
||
|
|
{viewingRequest.currency} {viewingRequest.total_amount?.toFixed(2) || '0.00'}
|
||
|
|
</strong>
|
||
|
|
</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="需求日期" span={1}>
|
||
|
|
{viewingRequest.expected_date || '-'}
|
||
|
|
</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="创建时间" span={1}>
|
||
|
|
{viewingRequest.created_at}
|
||
|
|
</Descriptions.Item>
|
||
|
|
{viewingRequest.remark && (
|
||
|
|
<Descriptions.Item label="备注" span={2}>{viewingRequest.remark}</Descriptions.Item>
|
||
|
|
)}
|
||
|
|
</Descriptions>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</Modal>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default PurchaseRequestsPage
|