Files
yunhaifinance/temp-clone/frontend/src/pages/CustomerDetail.tsx
T

284 lines
12 KiB
TypeScript

import React, { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import {
Card, Descriptions, Tag, Spin, Empty, Row, Col, Statistic, Table, Button, Divider, Typography, Badge
} from 'antd'
import {
ArrowLeftOutlined, HomeOutlined, FileTextOutlined, DollarOutlined, UserOutlined, PhoneOutlined
} from '@ant-design/icons'
import axios from 'axios'
const { Title, Text } = Typography
interface Contact {
name: string
position: string
phone: string
is_primary?: boolean
}
interface Customer {
id: number
code: string
name: string
address: string
contacts: Contact[]
remark: string
total_contract_amount: number
total_received: number
total_receivable: number
created_at: string
}
interface Project {
id: number
project_code: string
name: string
contract_amount: string
status: string
customer_id: number
}
interface PaymentNode {
id: number
project_id: number
amount: number
paid_amount: number
}
interface Quotation {
id: number
version: number
quotation_date: string
amount: number
currency: string
status: string
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?: string
intermediary_fee_value?: number
customer_requirements?: string
project_overview?: string
attachments?: string[]
survey_photos?: string[]
status: string
days_in_status: number
created_at: string
quotations: Quotation[]
}
const CustomerDetail: React.FC = () => {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const [customer, setCustomer] = useState<Customer | null>(null)
const [projects, setProjects] = useState<Project[]>([])
const [paymentNodes, setPaymentNodes] = useState<PaymentNode[]>([])
const [budgetProjects, setBudgetProjects] = useState<BudgetProject[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetchCustomerDetail()
fetchRelatedProjects()
fetchRelatedBudgetProjects()
}, [id])
const fetchCustomerDetail = async () => {
try {
const res = await fetch(`/api/customers/${id}`)
const data = await res.json()
if (data.success) setCustomer(data.data)
} catch (error) {
console.error('获取客户详情失败:', error)
} finally {
setLoading(false)
}
}
const fetchRelatedProjects = async () => {
try {
// 获取所有项目,筛选关联到此客户的
const res = await fetch('/api/projects')
const data = await res.json()
if (data.success) {
const customerProjects = (data.data || []).filter((p: Project) => p.customer_id === parseInt(id))
setProjects(customerProjects)
// 获取所有付款节点
const nodesRes = await fetch('/api/payment-nodes')
const nodesData = await nodesRes.json()
if (nodesData.success) {
setPaymentNodes(nodesData.data || [])
}
}
} catch (error) {
console.error('获取项目失败:', error)
}
}
const fetchRelatedBudgetProjects = async () => {
try {
// 获取与当前客户关联的预算项目
const res = await axios.get('/api/budget-projects', {
params: { customer_id: id }
})
if (res.data.success) {
setBudgetProjects(res.data.data || [])
}
} catch (error) {
console.error('获取预算项目失败:', error)
}
}
if (loading) return <Spin style={{ display: 'flex', justifyContent: 'center', padding: 50 }} />
if (!customer) return <Empty description="客户不存在" style={{ marginTop: 100 }} />
// 计算财务数据
const totalContract = projects.reduce((sum, p) => sum + (parseFloat(p.contract_amount) || 0), 0)
// 从付款节点计算已收金额
const projectIds = projects.map(p => p.id)
const relatedNodes = paymentNodes.filter(n => projectIds.includes(n.project_id))
const totalReceived = relatedNodes.reduce((sum, n) => sum + (n.paid_amount || 0), 0)
const totalReceivable = relatedNodes.reduce((sum, n) => sum + ((n.amount || 0) - (n.paid_amount || 0)), 0)
const projectColumns = [
{ title: '项目编号', dataIndex: 'project_code', key: 'project_code', width: 120 },
{ title: '项目名称', dataIndex: 'name', key: 'name', render: (v: string) => <Text strong>{v}</Text> },
{ title: '合同金额', dataIndex: 'contract_amount', key: 'contract_amount', align: 'right' as const, render: (v: string) => ${(parseFloat(v) || 0).toLocaleString()}` },
{ title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, render: (v: string) => <Badge status={v === 'completed' ? 'success' : 'processing'} text={v === 'completed' ? '已完成' : v === 'planning' ? '规划中' : v === 'in_progress' ? '进行中' : v} /> }
]
const budgetProjectColumns = [
{ title: '项目名称', dataIndex: 'name', key: 'name', render: (v: string, record: BudgetProject) => (
<Text strong onClick={() => navigate(`/budget-projects/${record.id}`)} style={{ cursor: 'pointer', color: '#1890ff' }}>
{v}
</Text>
) },
{ title: '业务经理', dataIndex: 'manager_name', key: 'manager_name' },
{ title: '状态', dataIndex: 'status', key: 'status', align: 'center' as const, render: (v: string) => {
const statusMap: Record<string, { status: 'success' | 'processing' | 'error' | 'default'; text: string }> = {
negotiating: { status: 'processing', text: '商谈中' },
signed: { status: 'success', text: '已签约' },
unsigned: { status: 'error', text: '未签约' }
}
const config = statusMap[v] || { status: 'default', text: v }
return <Badge status={config.status} text={config.text} />
} },
{ title: '报价版本数', dataIndex: 'quotations', key: 'quotations', align: 'center' as const, render: (quotations: Quotation[]) => (quotations || []).length },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', render: (v: string) => v.split('T')[0] }
]
return (
<div style={{ padding: '16px', maxWidth: 1200, margin: '0 auto' }}>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/customers')} style={{ marginBottom: 16 }} type="text">
返回列表
</Button>
<Title level={4} style={{ marginBottom: 24 }}>
<HomeOutlined style={{ marginRight: 8, color: '#52c41a' }} />
{customer.name}
</Title>
{/* ========== 卡片1:基本信息 ========== */}
<Card title={<><UserOutlined /> 基本信息</>} style={{ marginBottom: 24, borderRadius: 8 }} headStyle={{ background: '#f5f5f5', fontWeight: 600 }}>
<Descriptions bordered column={{ xs: 1, sm: 2 }} size="small">
<Descriptions.Item label="编号">{customer.code}</Descriptions.Item>
<Descriptions.Item label="地址">{customer.address || '-'}</Descriptions.Item>
</Descriptions>
{customer.remark && (
<>
<Divider style={{ margin: '16px 0' }} />
<div><Text type="secondary">备注:</Text><div style={{ marginTop: 8, padding: 12, background: '#fafafa', borderRadius: 4 }}>{customer.remark}</div></div>
</>
)}
<Divider style={{ margin: '16px 0' }} />
<div style={{ marginBottom: 8 }}><Text type="secondary"><PhoneOutlined style={{ marginRight: 4 }} />联系人</Text></div>
<Row gutter={[16, 16]}>
{(customer.contacts || []).map((contact, i) => (
<Col key={i} xs={24} sm={12} lg={8}>
<Card size="small" style={{ borderLeft: contact.is_primary ? '3px solid #52c41a' : '3px solid #d9d9d9', background: contact.is_primary ? '#f6ffed' : '#fff' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
<Text strong>{contact.name || '未命名'}</Text>
{contact.is_primary && <Tag color="green" size="small">主联系人</Tag>}
</div>
<div style={{ color: '#666', fontSize: 13 }}>
{contact.position && <div>职位:{contact.position}</div>}
{contact.phone && <div>电话:{contact.phone}</div>}
</div>
</Card>
</Col>
))}
</Row>
{(customer.contacts || []).length === 0 && <Empty description="暂无联系人" image={Empty.PRESENTED_IMAGE_SIMPLE} />}
</Card>
{/* ========== 卡片2:关联项目 ========== */}
<Card title={<><FileTextOutlined /> 关联项目 ({projects.length})</>} style={{ marginBottom: 24, borderRadius: 8 }} headStyle={{ background: '#f5f5f5', fontWeight: 600 }}>
{projects.length > 0 ? (
<Table columns={projectColumns} dataSource={projects} rowKey="id" size="small" pagination={false} bordered />
) : (
<Empty description="暂无关联项目(在项目管理中选择此客户后会自动显示)" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</Card>
{/* ========== 卡片4:关联预算项目 ========== */}
<Card title={<><DollarOutlined /> 关联预算项目 ({budgetProjects.length})</>} style={{ marginBottom: 24, borderRadius: 8 }} headStyle={{ background: '#f5f5f5', fontWeight: 600 }}>
{budgetProjects.length > 0 ? (
<Table columns={budgetProjectColumns} dataSource={budgetProjects} rowKey="id" size="small" pagination={false} bordered />
) : (
<Empty description="暂无关联预算项目(在预算报价管理中选择此客户后会自动显示)" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</Card>
{/* ========== 卡片3:财务信息 ========== */}
<Card title={<><DollarOutlined /> 财务信息</>} style={{ borderRadius: 8 }} headStyle={{ background: '#f5f5f5', fontWeight: 600 }}>
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
<Col xs={12} lg={6}>
<Card size="small" style={{ textAlign: 'center', background: '#e6f7ff', border: '1px solid #91d5ff' }}>
<Statistic title="合同总金额" value={totalContract} prefix="¥" valueStyle={{ color: '#1890ff', fontSize: 20 }} />
</Card>
</Col>
<Col xs={12} lg={6}>
<Card size="small" style={{ textAlign: 'center', background: '#f6ffed', border: '1px solid #b7eb8f' }}>
<Statistic title="已收总金额" value={totalReceived} prefix="¥" valueStyle={{ color: '#52c41a', fontSize: 20 }} />
</Card>
</Col>
<Col xs={12} lg={6}>
<Card size="small" style={{ textAlign: 'center', background: '#fff2f0', border: '1px solid #ffccc7' }}>
<Statistic title="应收总金额" value={totalReceivable} prefix="¥" valueStyle={{ color: '#ff4d4f', fontSize: 20 }} />
</Card>
</Col>
<Col xs={12} lg={6}>
<Card size="small" style={{ textAlign: 'center', background: '#fffbe6', border: '1px solid #ffe58f' }}>
<Statistic title="未结金额" value={totalReceivable} prefix="¥" valueStyle={{ color: '#faad14', fontSize: 20 }} />
</Card>
</Col>
</Row>
<Divider style={{ margin: '16px 0' }} />
<div style={{ marginBottom: 16 }}><Text type="secondary">项目明细</Text></div>
{projects.length > 0 ? (
<Table columns={projectColumns} dataSource={projects} rowKey="id" size="small" pagination={false} bordered />
) : (
<Empty description="暂无财务数据" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</Card>
</div>
)
}
export default CustomerDetail