/** * 采购申请页面 * 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.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([]) const [completedRequests, setCompletedRequests] = useState([]) const [activeTab, setActiveTab] = useState('active') const [loading, setLoading] = useState(false) const [projects, setProjects] = useState([]) const [selectedProjectId, setSelectedProjectId] = useState(null) const [selectedStatus, setSelectedStatus] = useState(null) const [modalVisible, setModalVisible] = useState(false) const [detailModalVisible, setDetailModalVisible] = useState(false) const [editingRequest, setEditingRequest] = useState(null) const [viewingRequest, setViewingRequest] = useState(null) const [currentEditingStatus, setCurrentEditingStatus] = useState('') const [purchaseType, setPurchaseType] = useState<'inventory' | 'project'>('inventory') const [currency, setCurrency] = useState('CNY') const [attachments, setAttachments] = useState([]) 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 = { 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 {info.text} } const columns: ColumnsType = [ { title: '事由', dataIndex: 'brief_description', key: 'brief_description', width: 180, ellipsis: true, render: (v: string, r: PurchaseRequest) => ( fetchRequestDetail(r.id)} style={{ fontWeight: 500 }}>{v || '-'} ) }, { 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 = { material: '材料', equipment: '设备', pole: '电杆', other: '其他' } return {categoryMap[category] || category} } }, { title: '预计金额', dataIndex: 'total_amount', key: 'total_amount', width: 130, align: 'right', render: (amount, record) => ( {record.currency} {amount?.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '0.00'} ) }, { 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) => {v || '-'} }, { title: '操作', key: 'actions', width: 200, fixed: 'right', render: (_, record) => ( )} {(record.status === 'withdrawn' || record.status === 'pending_edit') && ( <> handleDelete(record.id)} > }>
{/* 编辑/新建弹窗 - 简化版 */} setModalVisible(false)} footer={[ , , ]} width={700} >
{purchaseType === 'project' && ( )} { 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') }} > {/* 详情弹窗 */} setDetailModalVisible(false)} footer={[]} width={700} > {viewingRequest && (
{viewingRequest.request_code || viewingRequest.code} {getStatusTag(viewingRequest.status)} {viewingRequest.purchase_type === 'project' ? '项目采购' : '库存采购'} {viewingRequest.project_name || '-'} {viewingRequest.applicant} {viewingRequest.request_date} {viewingRequest.brief_description || '-'} {{ material: '材料', equipment: '设备', pole: '电杆', other: '其他' }[viewingRequest.expense_category] || viewingRequest.expense_category} {viewingRequest.currency} {viewingRequest.total_amount?.toFixed(2) || '0.00'} {viewingRequest.expected_date || '-'} {viewingRequest.created_at} {viewingRequest.remark && ( {viewingRequest.remark} )}
)}
) } export default PurchaseRequestsPage