Files
manage/src/pages/company/quotation/detail/index.jsx
2025-01-02 18:02:10 +08:00

541 lines
16 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, { useState, useEffect, useMemo } from "react";
import {
Form,
Input,
InputNumber,
Select,
Button,
Space,
Card,
Typography,
message,
Popconfirm,
Modal,
Divider,
} from "antd";
import {
PlusOutlined,
ArrowLeftOutlined,
SaveOutlined,
DeleteOutlined,
} from "@ant-design/icons";
import { supabase } from "@/config/supabase";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import SectionList from '@/components/SectionList'
import ChatAIDrawer from '@/components/ChatAi';
const { Title } = Typography;
// 添加货币符号映射
const CURRENCY_SYMBOLS = {
CNY: "¥",
TWD: "NT$",
USD: "$",
};
const QuotationForm = () => {
const { id } = useParams();
const [searchParams] = useSearchParams();
const isEdit = searchParams.get("edit") === "true";
const templateId = searchParams.get("templateId");
const isView = id && !isEdit;
const [form] = Form.useForm();
const navigate = useNavigate();
const [dataSource, setDataSource] = useState([{ id: Date.now() }]);
const [totalAmount, setTotalAmount] = useState(0);
const [loading, setLoading] = useState(false);
const [currentCurrency, setCurrentCurrency] = useState("TWD");
const [customers, setCustomers] = useState([]);
const [selectedCustomers, setSelectedCustomers] = useState([]);
const [formValues, setFormValues] = useState({});
const [templateModalVisible, setTemplateModalVisible] = useState(false);
const [availableSections, setAvailableSections] = useState([]);
const [editingSectionIndex, setEditingSectionIndex] = useState(null);
const [editingSectionName, setEditingSectionName] = useState("");
const [taxRate, setTaxRate] = useState(0);
const [discount, setDiscount] = useState(0);
// 计算单项金额
const calculateItemAmount = useMemo(
() => (quantity, price) => {
const safeQuantity = Number(quantity) || 0;
const safePrice = Number(price) || 0;
return safeQuantity * safePrice;
},
[]
);
// 计算小节总额
const calculateSectionTotal = useMemo(
() =>
(items = []) => {
if (!Array.isArray(items)) return 0;
return items.reduce((sum, item) => {
if (!item) return sum;
return sum + calculateItemAmount(item.quantity, item.price);
}, 0);
},
[calculateItemAmount]
);
// 计算总金额
const calculateTotalAmount = useMemo(
() =>
(sections = []) => {
if (!Array.isArray(sections)) return 0;
return sections.reduce((sum, section) => {
if (!section) return sum;
return sum + calculateSectionTotal(section.items);
}, 0);
},
[calculateSectionTotal]
);
// 格式化货币
const formatCurrency = useMemo(
() =>
(amount, currency = currentCurrency) => {
const safeAmount = Number(amount) || 0;
return `${CURRENCY_SYMBOLS[currency] || ""}${safeAmount.toLocaleString(
"zh-CN",
{
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}
)}`;
},
[currentCurrency]
);
useEffect(() => {
console.log(currentCurrency, 'currency');
}, [currentCurrency])
// 处理表单值变化
const handleValuesChange = (changedValues, allValues) => {
setFormValues(allValues);
if (changedValues.currency) {
setCurrentCurrency(changedValues.currency);
}
};
// 修改初始值确保每个项目都有唯一ID
const initialValues = {
currency: "TWD",
sections: [
{
key: uuidv4(),
sectionName: "服务类型 1",
items: [
{
key: uuidv4(),
name: "",
quantity: 1,
price: 0,
description: "",
unit: "",
},
],
},
],
};
const fetchQuotationDetail = async () => {
try {
setLoading(true);
const { data, error } = await supabase
.from("resources")
.select("*")
.eq("id", id)
.single();
if (error) throw error;
if (data?.attributes) {
const formData = {
quataName: data.attributes.quataName,
customers: data.attributes.customers.map((customer) => customer.id) || [],
description: data.attributes.description,
currency: data.attributes.currency || "TWD",
sections: data.attributes.sections.map((section) => ({
key: uuidv4(),
sectionName: section.sectionName,
items: section.items.map((item) => ({
key: uuidv4(),
name: item.name,
quantity: Number(item.quantity) || 0,
price: Number(item.price) || 0,
description: item.description || "",
unit: item.unit || "",
})),
})),
taxRate: data.attributes.taxRate || 0,
discount: data.attributes.discount || 0,
};
form.setFieldsValue(formData);
setFormValues(formData);
setCurrentCurrency(data.attributes.currency || "TWD");
setTaxRate(data.attributes.taxRate || 0);
setDiscount(data.attributes.discount || 0);
if (data.attributes.customers) {
setSelectedCustomers(data.attributes.customers);
}
}
} catch (error) {
console.error("获取报价单详情失败:", error);
message.error("获取报价单详情失败");
} finally {
setLoading(false);
}
};
const fetchTemplateData = async () => {
try {
setLoading(true);
const { data: template, error } = await supabase
.from("resources")
.select("*")
.eq("type", "serviceTemplate")
.eq("id", templateId)
.single();
if (error) throw error;
if (template?.attributes) {
const quotationData = {
quataName: template.attributes.templateName,
description: template.attributes.description,
currency: template.attributes.currency || "TWD",
category: template.attributes.category,
sections: template.attributes.sections.map((section) => ({
key: uuidv4(),
sectionName: section.sectionName,
items: section.items.map((item) => ({
key: uuidv4(),
name: item.name,
quantity: item.quantity,
price: item.price,
description: item.description,
unit: item.unit,
})),
})),
};
setCurrentCurrency(template.attributes.currency || "TWD");
form.setFieldsValue(quotationData);
setFormValues(quotationData);
}
} catch (error) {
console.error("获取模板数据失败:", error);
message.error("获取模板数据失败");
} finally {
setLoading(false);
}
};
// 使用 useMemo 计算税后金额
const afterTaxAmount = useMemo(() => {
const beforeTaxAmount = calculateTotalAmount(formValues?.sections) || 0;
const taxAmount = beforeTaxAmount * (taxRate / 100);
return beforeTaxAmount + taxAmount;
}, [formValues?.sections, taxRate, calculateTotalAmount]);
// 修改保存函数
const onFinish = async (values) => {
try {
setLoading(true);
const beforeTaxAmount = calculateTotalAmount(values.sections);
const quotationData = {
type: "quota",
attributes: {
quataName: values.quataName,
customers: customers
.filter(customer => values.customers.includes(customer.id))
.map(customer => ({
id: customer.id,
name: customer.attributes.name
})),
description: values.description,
currency: currentCurrency,
sections: values.sections.map((section) => ({
sectionName: section.sectionName,
items: section.items.map((item) => ({
name: item.name,
unit: item.unit,
price: item.price,
quantity: item.quantity,
description: item.description,
})),
})),
beforeTaxAmount,
taxRate,
afterTaxAmount,
discount,
finalAmount: discount || afterTaxAmount,
},
};
let result;
if (id) {
result = await supabase
.from("resources")
.update(quotationData)
.eq("id", id)
.select();
} else {
result = await supabase
.from("resources")
.insert([quotationData])
.select();
}
if (result.error) throw result.error;
message.success("保存成功");
navigate("/company/quotation");
} catch (error) {
console.error("保存失败:", error);
message.error("保存失败");
} finally {
setLoading(false);
}
};
const fetchCustomers = async () => {
try {
const { data, error } = await supabase
.from("resources")
.select("*")
.eq("type", "customer");
if (error) throw error;
setCustomers(data || []);
} catch (error) {
console.error("获取客户列表失败:", error);
}
};
useEffect(() => {
fetchCustomers();
}, []);
// 确保在组件加载时正确获取数据
useEffect(() => {
if (id) {
fetchQuotationDetail();
} else if (templateId) {
fetchTemplateData();
} else {
// 如果既不是编辑也不是从模板创建,则设置初始值
form.setFieldsValue(initialValues);
setFormValues(initialValues);
}
}, [id, templateId]);
const [open, setOpen] = useState(false);
const handleExport = (data) => {
if(data?.activityName&&data?.currency){
const quotationData = {
quataName: data.activityName,
description: data.description,
currency: data.currency || "TWD",
sections: data.sections.map((section) => ({
key: uuidv4(),
sectionName: section.sectionName,
items: section.items.map((item) => ({
key: uuidv4(),
name: item.name,
quantity: item.quantity,
price: item.price,
description: item.description,
unit: item.unit,
})),
})),
};
setCurrentCurrency(data.currency || "TWD");
form.setFieldsValue(quotationData);
setFormValues(quotationData);
setTaxRate(data.taxRate || 0);
message.success('已添加报价单');
}else{
const _data={
...data,
key: uuidv4(),
}
const newSections = [...formValues.sections, _data];
form.setFieldValue('sections', newSections);
const currentFormValues = form.getFieldsValue();
setFormValues({
...currentFormValues,
sections: newSections
});
message.success('已添加新的服务项目');
}
setOpen(false);
};
return (
<div className="bg-gradient-to-b from-gray-50 to-white dark:from-gray-800 dark:to-gray-900/90 min-h-screen p-2">
<Card
className="shadow-lg rounded-lg border-0"
title={
<div className="flex justify-between items-center py-2">
<div className="flex items-center space-x-3">
<Title level={4} className="mb-0 text-gray-800">
{id ? (isEdit ? "编辑报价单" : "查看报价单") : "新建报价单"}
</Title>
<span className="text-gray-400 text-sm">
{id
? isEdit
? "请修改报价单信息"
: "报价单详情"
: "请填写报价单信息"}
</span>
</div>
<Space size="middle">
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate("/company/quotation")}
>
返回
</Button>
{!isView && (
<>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => form.submit()}
loading={loading}
>
保存
</Button>
<Button onClick={() => setOpen(true)}>
AI 助手
</Button>
</>
)}
</Space>
</div>
}
>
<Form
form={form}
onFinish={onFinish}
onValuesChange={handleValuesChange}
layout="vertical"
disabled={isView}
initialValues={initialValues}
>
{/* 基本信息卡片 */}
<Card
className="shadow-sm rounded-lg"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>基本信息</span>
</span>
}
bordered={false}
>
<div className="grid grid-cols-2 gap-8">
<Form.Item
name="quataName"
label={
<span className="text-gray-700 font-medium">活动名称</span>
}
rules={[{ required: true, message: "活动名称" }]}
>
<Input
placeholder="请输入活动名"
className=" hover:border-blue-400 focus:border-blue-500"
/>
</Form.Item>
<Form.Item
name="currency"
label={<span className="text-gray-700 font-medium">货币类型</span>}
rules={[{ required: true, message: "请选择货币类型" }]}
>
<Select className="w-full">
<Select.Option value="CNY">RMB</Select.Option>
<Select.Option value="TWD">台币</Select.Option>
<Select.Option value="USD">美元</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="customers"
label={<span className="text-gray-700 font-medium">客户名称</span>}
rules={[{ required: true, message: "请选择至少一个客户" }]}
>
<Select
mode="multiple"
placeholder="请选择客户"
className=" hover:border-blue-400 focus:border-blue-500"
showSearch
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? "").toLowerCase().includes(input.toLowerCase())
}
options={customers.map((customer) => ({
value: customer.id,
label: customer.attributes.name,
}))}
value={(formValues.customers || []).map(customer => customer.id)}
/>
</Form.Item>
</div>
</Card>
<Card
className="shadow-sm rounded-lg mt-6"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>服务明细</span>
</span>
}
bordered={false}
>
<SectionList
type="quotation"
form={form}
isView={isView}
formValues={formValues}
currentCurrency={currentCurrency}
taxRate={taxRate}
setTaxRate={setTaxRate}
/>
</Card>
</Form>
</Card>
<ChatAIDrawer
open={open}
onClose={() => setOpen(false)}
onExport={handleExport}
/>
</div>
);
};
export default QuotationForm;