quata模块完成

This commit is contained in:
‘Liammcl’
2024-12-22 23:20:20 +08:00
parent df0aa520ca
commit 98eb405cc5
14 changed files with 2822 additions and 609 deletions

View File

@@ -1,18 +1,28 @@
import React, { useEffect, useState } from 'react';
import { Card, Table, Button, message, Popconfirm, Tag, Space, Tooltip } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,CopyOutlined } from '@ant-design/icons';
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 { formatCurrency } from '@/utils/format'; // 假设你有这个工具函数,如果没有我会提供
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,
loading: loadingQuotations,
total,
fetchResources: fetchQuotations,
deleteResource: deleteQuotation
@@ -43,6 +53,68 @@ const QuotationPage = () => {
}
};
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: '报价单名称',
@@ -52,24 +124,59 @@ const QuotationPage = () => {
},
{
title: '客户信息',
dataIndex: ['attributes', 'customerName'],
key: 'customerName',
render: (customerName,record) => (
<Tag color="blue" className='cursor-pointer' onClick={() => {
navigate(`/company/customerInfo/${record.attributes.customerId}`)
}}>{customerName}</Tag>
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', 'totalAmount'],
dataIndex: ['attributes'],
key: 'totalAmount',
align: 'right',
render: (amount) => (
<span style={{ color: '#f50', fontWeight: 'bold' }}>
¥{amount?.toLocaleString()}
</span>
),
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: '创建日期',
@@ -135,40 +242,164 @@ const QuotationPage = () => {
},
];
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={() => navigate('/company/quotaInfo')}
>
新增报价单
</Button>
}
>
<Table
columns={columns}
dataSource={quotations}
rowKey="id"
loading={loading}
onChange={handleTableChange}
pagination={{
...pagination,
total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`,
<>
<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);
}}
/>
</Card>
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>
</>
);
};