Files
manage/src/pages/company/quotation/index.jsx
2024-12-22 23:20:20 +08:00

406 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;