import React, { useState, useEffect } from 'react'; import { Card, Typography, Button, Space, Tag, message, Empty, Radio, Popconfirm } from 'antd'; import { PlusOutlined, EyeOutlined, EditOutlined, DeleteOutlined, DownOutlined, RightOutlined, FileAddOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import axios from 'axios'; import dayjs from 'dayjs'; import { useAuthStore } from '../../store/authStore'; import QuotationCreateModal from './QuotationCreateModal'; const { Title, Paragraph, Text } = Typography; interface Quotation { id: number; version: number; quotation_date: string; amount: number; currency: string; status: 'draft' | 'sent' | 'approved' | 'rejected'; file_url?: string; remark?: string; created_at: string; } interface BudgetProject { id: number; name: string; customer_id: number; customer_name: string; manager_id: number; manager_name: string; location?: string; survey_date?: string; intermediary?: string; intermediary_fee_type?: 'fixed' | 'percentage'; intermediary_fee_value?: number; customer_requirements?: string; project_overview?: string; attachments?: string[]; survey_photos?: string[]; status: 'negotiating' | 'signed' | 'unsigned'; days_in_status: number; created_at: string; quotations: Quotation[]; } type StatusFilter = 'all' | 'negotiating' | 'signed' | 'unsigned'; const CURRENCIES: Record = { CNY: { label: '人民币', symbol: '¥' }, USD: { label: '美元', symbol: '$' }, LAK: { label: '老挝基普', symbol: '₭' }, THB: { label: '泰铢', symbol: '฿' }, }; const BudgetProjectList: React.FC = () => { const [isMobile, setIsMobile] = useState(false); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(false); const [statusFilter, setStatusFilter] = useState('all'); const [expandedKeys, setExpandedKeys] = useState>(new Set()); const [quotationModalVisible, setQuotationModalVisible] = useState(false); const [selectedProject, setSelectedProject] = useState(null); const navigate = useNavigate(); const { user: _currentUser } = useAuthStore(); // const isAdmin = _currentUser?.role === 'admin'; useEffect(() => { const checkMobile = () => setIsMobile(window.innerWidth <= 768); checkMobile(); window.addEventListener('resize', checkMobile); return () => window.removeEventListener('resize', checkMobile); }, []); useEffect(() => { fetchProjects(); }, []); const fetchProjects = async () => { setLoading(true); try { const res = await axios.get('/api/budget-projects'); if (res.data.success) { setProjects(res.data.data); } } catch (error) { console.error('获取预算项目失败:', error); message.error('获取数据失败'); } finally { setLoading(false); } }; const filteredProjects = projects.filter(p => statusFilter === 'all' || p.status === statusFilter ); const toggleExpand = (id: number) => { const newSet = new Set(expandedKeys); if (newSet.has(id)) { newSet.delete(id); } else { newSet.add(id); } setExpandedKeys(newSet); }; const getStatusTag = (status: string) => { const statusMap: Record = { negotiating: { color: 'processing', text: '商谈中' }, signed: { color: 'success', text: '已签约' }, unsigned: { color: 'error', text: '未签约' }, }; const config = statusMap[status] || { color: 'default', text: status }; return {config.text}; }; const getQuotationStatusTag = (status: string) => { const statusMap: Record = { draft: { color: 'default', text: '草稿' }, sent: { color: 'processing', text: '已发送' }, approved: { color: 'success', text: '已通过' }, rejected: { color: 'error', text: '已拒绝' }, }; const config = statusMap[status] || { color: 'default', text: status }; return {config.text}; }; const formatAmount = (amount: number, currency: string = 'CNY') => { const c = CURRENCIES[currency]; const symbol = c?.symbol || '¥'; return `${symbol}${amount.toLocaleString('zh-CN')}`; }; const handleSign = async (projectId: number) => { try { const res = await axios.put(`/api/budget-projects/${projectId}/sign`); if (res.data.success) { message.success('标记签约成功'); fetchProjects(); } } catch (error) { message.error('操作失败'); } }; const handleUnsigned = async (projectId: number) => { try { const res = await axios.put(`/api/budget-projects/${projectId}/unsigned`); if (res.data.success) { message.success('标记未签约成功'); fetchProjects(); } } catch (error) { message.error('操作失败'); } }; const handleDeleteQuotation = async (projectId: number, quotationId: number) => { try { const res = await axios.delete(`/api/budget-projects/${projectId}/quotations/${quotationId}`); if (res.data.success) { message.success('删除成功'); fetchProjects(); } } catch (error) { message.error('删除失败'); } }; const openQuotationModal = (project: BudgetProject) => { setSelectedProject(project); setQuotationModalVisible(true); }; const handleQuotationSuccess = () => { setQuotationModalVisible(false); fetchProjects(); }; const goToProjectManagement = (projectId: number) => { navigate(`/projects/${projectId}`); }; return (
预算报价管理 管理商谈项目及报价版本
{/* 状态筛选 */} 状态筛选: setStatusFilter(e.target.value)} optionType="button" buttonStyle="solid" > 全部 商谈中 已签约 未签约 {/* 项目列表 */} {filteredProjects.length === 0 ? ( ) : (
{filteredProjects.map((project) => (
{/* 项目头部 */}
toggleExpand(project.id)} >
{expandedKeys.has(project.id) ? : } {project.name} {getStatusTag(project.status)} {project.days_in_status}天
客户: {project.customer_name} 业务经理: {project.manager_name} {project.intermediary && ( 居间人: {project.intermediary} {project.intermediary_fee_value && ( 居间费: {formatAmount(project.intermediary_fee_value, 'CNY')} )} )}
{/* 展开内容 - 报价版本 */} {expandedKeys.has(project.id) && (
{project.quotations && project.quotations.length > 0 ? (
{project.quotations.map((quotation, index) => (
报价V{quotation.version} {dayjs(quotation.quotation_date).format('YYYY-MM-DD')} {formatAmount(quotation.amount, quotation.currency)} {getQuotationStatusTag(quotation.status)} {quotation.status === 'draft' && ( )} handleDeleteQuotation(project.id, quotation.id)} >
))}
) : ( )} {/* 操作按钮 */} {project.status === 'negotiating' && (
)} {project.status === 'signed' && (
)}
)}
))}
)}
{/* 新增报价版本弹窗 */} setQuotationModalVisible(false)} onSuccess={handleQuotationSuccess} />
); }; export default BudgetProjectList;