406 lines
12 KiB
JavaScript
406 lines
12 KiB
JavaScript
import React, { useEffect, useState } from 'react';
|
||
import { Card, Table, Button, message, Popconfirm, Tag, Space, Spin, Modal, Empty, Select, Typography, Statistic, Divider } from 'antd';
|
||
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,CopyOutlined, FileAddOutlined, AppstoreOutlined } from '@ant-design/icons';
|
||
import { useResources } from '@/hooks/resource/useResource';
|
||
import { useNavigate } from 'react-router-dom';
|
||
import {supabase}from '@/config/supabase'
|
||
const CURRENCY_SYMBOLS = {
|
||
CNY: "¥",
|
||
TWD: "NT$",
|
||
USD: "$",
|
||
};
|
||
const QuotationPage = () => {
|
||
const navigate = useNavigate();
|
||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
|
||
const [sorter, setSorter] = useState({ field: 'created_at', order: 'descend' });
|
||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||
const [selectedTemplateId, setSelectedTemplateId] = useState(null);
|
||
const [templates, setTemplates] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||
const [categories, setCategories] = useState([]);
|
||
|
||
const {
|
||
resources: quotations,
|
||
loading: loadingQuotations,
|
||
total,
|
||
fetchResources: fetchQuotations,
|
||
deleteResource: deleteQuotation
|
||
} = useResources(pagination, sorter, 'quota');
|
||
|
||
useEffect(() => {
|
||
fetchQuotations();
|
||
}, []);
|
||
|
||
const handleTableChange = (pagination, filters, sorter) => {
|
||
setPagination(pagination);
|
||
setSorter(sorter);
|
||
fetchQuotations({
|
||
current: pagination.current,
|
||
pageSize: pagination.pageSize,
|
||
field: sorter.field,
|
||
order: sorter.order,
|
||
});
|
||
};
|
||
|
||
const handleDelete = async (id) => {
|
||
try {
|
||
await deleteQuotation(id);
|
||
message.success('删除成功');
|
||
fetchQuotations();
|
||
} catch (error) {
|
||
message.error('删除失败:' + error.message);
|
||
}
|
||
};
|
||
|
||
const fetchTemplates = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const { data: services, error } = await supabase
|
||
.from("resources")
|
||
.select("*")
|
||
.eq("type", "serviceTemplate")
|
||
.order("created_at", { ascending: false });
|
||
|
||
if (error) throw error;
|
||
setTemplates(services);
|
||
} catch (error) {
|
||
console.error("获取服务模板失败:", error);
|
||
message.error("获取服务模板失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (isModalVisible) {
|
||
fetchTemplates();
|
||
}
|
||
}, [isModalVisible]);
|
||
|
||
useEffect(() => {
|
||
if (templates.length > 0) {
|
||
setCategories(getAllCategories(templates));
|
||
}
|
||
}, [templates]);
|
||
|
||
const handleTemplateSelect = (templateId) => {
|
||
setSelectedTemplateId(templateId);
|
||
};
|
||
|
||
const handleConfirm = () => {
|
||
if (selectedTemplateId) {
|
||
navigate(`/company/quotaInfo?templateId=${selectedTemplateId}`);
|
||
} else {
|
||
navigate('/company/quotaInfo');
|
||
}
|
||
setIsModalVisible(false);
|
||
setSelectedTemplateId(null);
|
||
};
|
||
|
||
const getAllCategories = (templates) => {
|
||
const categorySet = new Set();
|
||
templates.forEach(template => {
|
||
template.attributes.category?.forEach(cat => {
|
||
categorySet.add(JSON.stringify(cat));
|
||
});
|
||
});
|
||
return Array.from(categorySet).map(cat => JSON.parse(cat));
|
||
};
|
||
|
||
const getFilteredTemplates = () => {
|
||
if (selectedCategory === 'all') return templates;
|
||
return templates.filter(template =>
|
||
template.attributes.category?.some(cat => cat.id === selectedCategory)
|
||
);
|
||
};
|
||
|
||
const columns = [
|
||
{
|
||
title: '报价单名称',
|
||
dataIndex: ['attributes', 'quataName'],
|
||
key: 'quataName',
|
||
ellipsis: true,
|
||
},
|
||
{
|
||
title: '客户信息',
|
||
dataIndex: ['attributes', 'customers'],
|
||
key: 'customers',
|
||
render: (customers,record) => (
|
||
<Space>
|
||
{customers?.map(customer => (
|
||
<Tag
|
||
key={customer.id}
|
||
color="blue"
|
||
className='cursor-pointer'
|
||
onClick={() => {
|
||
navigate(`/company/customerInfo/${customer.id}`)
|
||
}}
|
||
>
|
||
{customer.name}
|
||
</Tag>
|
||
))}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '报价总额',
|
||
dataIndex: ['attributes'],
|
||
key: 'totalAmount',
|
||
align: 'right',
|
||
width: 200,
|
||
render: (attributes) => {
|
||
// 获取货币符号
|
||
const currencySymbol = CURRENCY_SYMBOLS[attributes.currency] || '¥';
|
||
|
||
return (
|
||
<div className="space-y-1">
|
||
<div className="flex justify-between items-center text-sm">
|
||
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
|
||
税前:{currencySymbol}{attributes.beforeTaxAmount?.toLocaleString()}
|
||
</Typography.Text>
|
||
</div>
|
||
<div className="flex justify-between items-center">
|
||
<Statistic
|
||
value={attributes.afterTaxAmount}
|
||
prefix={currencySymbol}
|
||
precision={2}
|
||
valueStyle={{
|
||
fontSize: '16px',
|
||
fontWeight: 600,
|
||
color: '#1890ff'
|
||
}}
|
||
className="!mb-0"
|
||
/>
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
title: '创建日期',
|
||
dataIndex: 'created_at',
|
||
key: 'created_at',
|
||
sorter: true,
|
||
width: 120,
|
||
render: (text) => (
|
||
<span>{new Date(text).toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
})}</span>
|
||
),
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width:250,
|
||
render: (_, record) => (
|
||
<Space size={0}>
|
||
<Button
|
||
size='small'
|
||
type="link"
|
||
icon={<CopyOutlined />}
|
||
|
||
// onClick={() => navigate(`/company/quotaInfo/preview/${record.id}`)}
|
||
>
|
||
复制
|
||
</Button>
|
||
<Button
|
||
size='small'
|
||
type="link"
|
||
icon={<EyeOutlined />}
|
||
onClick={() => navigate(`/company/quotaInfo/preview/${record.id}`)}
|
||
>
|
||
查看
|
||
</Button>
|
||
<Button
|
||
size='small'
|
||
type="link"
|
||
icon={<EditOutlined />}
|
||
onClick={() => navigate(`/company/quotaInfo/${record.id}?edit=true`)}
|
||
>
|
||
编辑
|
||
</Button>
|
||
<Popconfirm
|
||
title="确定要删除这个报价单吗?"
|
||
description="删除后将无法恢复!"
|
||
onConfirm={() => handleDelete(record.id)}
|
||
okText="确定"
|
||
cancelText="取消"
|
||
okButtonProps={{ danger: true }}
|
||
>
|
||
<Button size='small' type="link" danger icon={<DeleteOutlined />}>
|
||
删除
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
const getTemplatesByCategory = () => {
|
||
const groups = new Map();
|
||
|
||
// 添加未分类组
|
||
groups.set('uncategorized', {
|
||
name: '未分类',
|
||
templates: templates.filter(t => !t.attributes.category || t.attributes.category.length === 0)
|
||
});
|
||
|
||
// 按分类分组
|
||
templates.forEach(template => {
|
||
if (template.attributes.category) {
|
||
template.attributes.category.forEach(cat => {
|
||
if (!groups.has(cat.id)) {
|
||
groups.set(cat.id, {
|
||
name: cat.name,
|
||
templates: []
|
||
});
|
||
}
|
||
groups.get(cat.id).templates.push(template);
|
||
});
|
||
}
|
||
});
|
||
|
||
return Array.from(groups.values()).filter(group => group.templates.length > 0);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<Card
|
||
title={
|
||
<Space>
|
||
<span>报价单管理</span>
|
||
<Tag color="blue">{total} 个报价单</Tag>
|
||
</Space>
|
||
}
|
||
className='h-full w-full overflow-auto'
|
||
extra={
|
||
<Button
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
onClick={() => setIsModalVisible(true)}
|
||
>
|
||
新增报价单
|
||
</Button>
|
||
}
|
||
>
|
||
<Table
|
||
columns={columns}
|
||
dataSource={quotations}
|
||
rowKey="id"
|
||
loading={loadingQuotations}
|
||
onChange={handleTableChange}
|
||
pagination={{
|
||
...pagination,
|
||
total,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: (total) => `共 ${total} 条记录`,
|
||
}}
|
||
/>
|
||
</Card>
|
||
|
||
<Modal
|
||
title="选择报价单模板"
|
||
open={isModalVisible}
|
||
onCancel={() => {
|
||
setIsModalVisible(false);
|
||
setSelectedTemplateId(null);
|
||
}}
|
||
footer={[
|
||
<Button key="custom" onClick={() => handleConfirm()}>
|
||
<FileAddOutlined /> 自定义创建
|
||
</Button>,
|
||
<Button
|
||
key="submit"
|
||
type="primary"
|
||
disabled={!selectedTemplateId}
|
||
onClick={handleConfirm}
|
||
>
|
||
<AppstoreOutlined /> 使用选中模板
|
||
</Button>,
|
||
]}
|
||
width={800}
|
||
>
|
||
{loading ? (
|
||
<div className="flex justify-center items-center h-[400px]">
|
||
<Spin size="large" />
|
||
</div>
|
||
) : templates.length === 0 ? (
|
||
<Empty description="暂无可用模板" />
|
||
) : (
|
||
<div className="max-h-[600px] overflow-y-auto px-1">
|
||
{getTemplatesByCategory().map((group, groupIndex) => (
|
||
<div key={groupIndex} className="mb-6 last:mb-2">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<div className="h-6 w-1 bg-blue-500 rounded-full"></div>
|
||
<h3 className="text-base font-medium text-gray-700">
|
||
{group.name}
|
||
<span className="ml-2 text-sm text-gray-400 font-normal">
|
||
({group.templates.length})
|
||
</span>
|
||
</h3>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-3">
|
||
{group.templates.map(template => (
|
||
<div
|
||
key={template.id}
|
||
className={`
|
||
p-3 border rounded-lg cursor-pointer transition-all
|
||
${selectedTemplateId === template.id
|
||
? 'border-blue-500 bg-blue-50/50'
|
||
: 'border-gray-200 hover:border-blue-300 hover:bg-gray-50/50'
|
||
}
|
||
`}
|
||
onClick={() => handleTemplateSelect(template.id)}
|
||
>
|
||
<div className="flex justify-between items-start gap-2 mb-2">
|
||
<div className="flex-1 min-w-0">
|
||
<h4 className="font-medium text-gray-800 truncate">
|
||
{template.attributes.templateName}
|
||
</h4>
|
||
<p className="text-xs text-gray-500 mt-1 line-clamp-1">
|
||
{template.attributes.description || '暂无描述'}
|
||
</p>
|
||
</div>
|
||
<span className="text-red-500 font-medium whitespace-nowrap text-sm">
|
||
¥{template.attributes.totalAmount?.toLocaleString()}
|
||
</span>
|
||
</div>
|
||
|
||
<div className="space-y-1">
|
||
{template.attributes.sections.map((section, index) => (
|
||
<div
|
||
key={index}
|
||
className="bg-white/80 px-2 py-1 rounded text-xs border border-gray-100"
|
||
>
|
||
<div className="flex justify-between items-center">
|
||
<span className="font-medium text-blue-600 truncate flex-1">
|
||
{section.sectionName}
|
||
</span>
|
||
<span className="text-gray-400 ml-1">
|
||
{section.items.length}项
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default QuotationPage; |