quata模块完成
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user