更换schema,服务管理50%

This commit is contained in:
‘Liammcl’
2024-12-22 12:33:02 +08:00
parent 36db1f29b0
commit df0aa520ca
4 changed files with 322 additions and 91 deletions

View File

@@ -21,7 +21,7 @@ export const createSupabase = () => {
detectSessionInUrl: false, detectSessionInUrl: false,
}, },
db: { db: {
schema: 'limq' schema: 'limq_dev'
} }
} }
); );

View File

@@ -22,7 +22,6 @@ const Login = () => {
const handleGoogleLogin = async () => { const handleGoogleLogin = async () => {
try { try {
await signInWithGoogle(); await signInWithGoogle();
navigate("/");
} catch (error) { } catch (error) {
console.error("Google login error:", error); console.error("Google login error:", error);
} }

View File

@@ -30,9 +30,10 @@ const ServiceForm = () => {
const isEdit = location.search.includes("edit=true"); const isEdit = location.search.includes("edit=true");
const [editingSectionIndex, setEditingSectionIndex] = useState(null); const [editingSectionIndex, setEditingSectionIndex] = useState(null);
const [availableSections, setAvailableSections] = useState([]); const [availableSections, setAvailableSections] = useState([]);
const [formValues, setFormValues] = useState({}); const [formValues, setFormValues] = useState({
const [sectionNameForm] = Form.useForm(); sections: [{ items: [{}] }],
currency: "CNY"
});
useEffect(() => { useEffect(() => {
if (id) { if (id) {
fetchServiceTemplate(); fetchServiceTemplate();
@@ -50,11 +51,13 @@ const ServiceForm = () => {
.single(); .single();
if (error) throw error; if (error) throw error;
form.setFieldsValue({ const formData = {
templateName: data.attributes.templateName, templateName: data.attributes.templateName,
description: data.attributes.description, description: data.attributes.description,
sections: data.attributes.sections, sections: data.attributes.sections,
}); };
form.setFieldsValue(formData);
setFormValues(formData);
} catch (error) { } catch (error) {
console.error("获取服务模版失败:", error); console.error("获取服务模版失败:", error);
message.error("获取服务模版失败"); message.error("获取服务模版失败");
@@ -96,6 +99,7 @@ const ServiceForm = () => {
templateName: values.templateName, templateName: values.templateName,
description: values.description, description: values.description,
sections: values.sections, sections: values.sections,
category: values.category || [], // 添加 category 字段
totalAmount, totalAmount,
}, },
}; };
@@ -279,7 +283,7 @@ const ServiceForm = () => {
disabled={id && !isEdit} disabled={id && !isEdit}
onValuesChange={handleValuesChange} onValuesChange={handleValuesChange}
> >
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6"> <div className="bg-white dark:bg-gray-800 p-2 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 mb-6">
<Title level={5} className="mb-4 dark:text-gray-200"> <Title level={5} className="mb-4 dark:text-gray-200">
基本信息 基本信息
</Title> </Title>
@@ -327,23 +331,12 @@ const ServiceForm = () => {
</div> </div>
</div> </div>
<div className="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700"> <div className="bg-white dark:bg-gray-800 p-2 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<Title level={5} className="!mb-0 dark:text-gray-200"> <Title level={5} className="!mb-0 dark:text-gray-200">
报价明细 报价明细
</Title> </Title>
{(!id || isEdit) && (
<Select
style={{ width: 200 }}
placeholder="选择已有小节"
onChange={handleAddExistingSection}
options={availableSections.map((section) => ({
value: section.id,
label: section.attributes.sectionName,
}))}
className="w-48"
/>
)}
</div> </div>
<Form.List name="sections"> <Form.List name="sections">
@@ -448,12 +441,7 @@ const ServiceForm = () => {
{...itemField} {...itemField}
name={[itemField.name, "name"]} name={[itemField.name, "name"]}
className="!mb-0" className="!mb-0"
rules={[
{
required: true,
message: "请输入服务项目名称",
},
]}
> >
<Input placeholder="服务项目名称" /> <Input placeholder="服务项目名称" />
</Form.Item> </Form.Item>

View File

@@ -1,9 +1,11 @@
import { Card, Table, Button, Space, Input, message, Popconfirm } from "antd"; import { Card, Table, Button, Space, Input, message, Popconfirm, Form, Tag, Typography } from "antd";
import { EyeOutlined } from "@ant-design/icons"; import { DownOutlined, UpOutlined,EyeOutlined,EditOutlined } from "@ant-design/icons";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { supabase } from "@/config/supabase"; import { supabase } from "@/config/supabase";
const { Paragraph } = Typography;
const ServicePage = () => { const ServicePage = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState([]); const [data, setData] = useState([]);
@@ -30,32 +32,72 @@ const ServicePage = () => {
} }
}; };
// 添加保存编辑项目的方法 // 修改保存逻辑
const handleSaveItem = async (record, sectionIndex, itemIndex) => { const handleSaveItem = async (record) => {
try { try {
setLoading(true); setLoading(true);
const newData = [...data]; const { serviceId, sectionIndex, itemIndex } = record;
const targetService = newData.find(item => item.id === record.serviceId);
targetService.attributes.sections[sectionIndex].items[itemIndex] = record;
// 计算新的总金额 // 找到当前服务模板
const newTotalAmount = targetService.attributes.sections.reduce((total, section) => { const currentService = data.find(item => item.id === serviceId);
return total + section.items.reduce((sectionTotal, item) => if (!currentService) {
sectionTotal + (item.price * item.quantity), 0); throw new Error('Service not found');
}, 0); }
targetService.attributes.totalAmount = newTotalAmount;
// 创建新的 attributes 对象,避免直接修改状态
const newAttributes = {
...currentService.attributes,
templateName: currentService.attributes.templateName,
description: currentService.attributes.description,
category: currentService.attributes.category || [], // 确保 category 字段存在
sections: currentService.attributes.sections.map((section, secIdx) => {
if (secIdx === sectionIndex) {
return {
...section,
items: section.items.map((item, itemIdx) => {
if (itemIdx === itemIndex) {
return {
name: record.name,
price: Number(record.price) || 0,
quantity: Number(record.quantity) || 0,
description: record.description || '',
unit: record.unit || ''
};
}
return item;
})
};
}
return section;
})
};
// 重新计算总金额
newAttributes.totalAmount = newAttributes.sections.reduce((total, section) => {
return total + section.items.reduce((sectionTotal, item) =>
sectionTotal + ((Number(item.price) || 0) * (Number(item.quantity) || 0)), 0);
}, 0);
// 调用 supabase 更新数据
const { error } = await supabase const { error } = await supabase
.from('resources') .from('resources')
.update({ attributes: targetService.attributes }) .update({
.eq('id', targetService.id); type: "serviceTemplate",
attributes: newAttributes
})
.eq('id', serviceId);
if (error) throw error; if (error) throw error;
setData(newData); // 更新本地状态
setData(prevData => prevData.map(item =>
item.id === serviceId
? { ...item, attributes: newAttributes }
: item
));
setEditingKey(''); setEditingKey('');
message.success('更新成功'); message.success('保存成功');
} catch (error) { } catch (error) {
console.error('保存失败:', error); console.error('保存失败:', error);
message.error('保存失败'); message.error('保存失败');
@@ -68,28 +110,73 @@ const ServicePage = () => {
const handleDeleteItem = async (record) => { const handleDeleteItem = async (record) => {
try { try {
setLoading(true); setLoading(true);
const newData = [...data]; const { serviceId, sectionIndex, itemIndex } = record;
const targetService = newData.find(item => item.id === record.serviceId);
// 删除指定项目 // 找到目标服务
targetService.attributes.sections[record.sectionIndex].items.splice(record.itemIndex, 1); const targetService = data.find(item => item.id === serviceId);
if (!targetService) throw new Error('Service not found');
// 重新计算总金额 // 创建新的 attributes 对象,避免直接修改状态
const newTotalAmount = targetService.attributes.sections.reduce((total, section) => { const newAttributes = {
return total + section.items.reduce((sectionTotal, item) => ...targetService.attributes,
sectionTotal + (item.price * item.quantity), 0); sections: targetService.attributes.sections.map((section, secIdx) => {
}, 0); if (secIdx === sectionIndex) {
return {
targetService.attributes.totalAmount = newTotalAmount; ...section,
items: section.items.filter((_, idx) => idx !== itemIndex)
};
}
return section;
})
};
// 重新计算总金额
newAttributes.totalAmount = newAttributes.sections.reduce((total, section) => {
return total + section.items.reduce((sectionTotal, item) =>
sectionTotal + ((Number(item.price) || 0) * (Number(item.quantity) || 0)), 0);
}, 0);
// 调用 supabase 更新数据
const { error } = await supabase const { error } = await supabase
.from('resources') .from('resources')
.update({ attributes: targetService.attributes }) .update({
.eq('id', targetService.id); attributes: newAttributes
})
.eq('id', serviceId);
if (error) throw error; if (error) throw error;
setData(newData); // 更新本地状态
setData(prevData => prevData.map(item =>
item.id === serviceId
? { ...item, attributes: newAttributes }
: item
));
message.success('删除成功');
} catch (error) {
console.error('删除失败:', error);
message.error('删<><E588A0><EFBFBD>失败');
} finally {
setLoading(false);
}
};
// 添加删除服务模板的方法
const handleDeleteService = async (serviceId) => {
try {
setLoading(true);
// 调用 supabase 删除数据
const { error } = await supabase
.from('resources')
.delete()
.eq('id', serviceId);
if (error) throw error;
// 更新本地状态
setData(prevData => prevData.filter(item => item.id !== serviceId));
message.success('删除成功'); message.success('删除成功');
} catch (error) { } catch (error) {
console.error('删除失败:', error); console.error('删除失败:', error);
@@ -107,10 +194,65 @@ const ServicePage = () => {
key: "templateName", key: "templateName",
className: "min-w-[200px]", className: "min-w-[200px]",
}, },
{
title: "分类",
dataIndex: ["attributes", "category"],
key: "category",
render: (category) => {
if (!category || !Array.isArray(category)) return null;
return (
<Paragraph
ellipsis={{
rows: 1,
tooltip: (
<Space size={[0, 8]} wrap>
{category.map((cat, index) => (
<Tag
size="small"
key={`${cat}-${index}`}
color="blue"
className="px-2 py-1 rounded-md"
>
{cat}
</Tag>
))}
</Space>
)
}}
className="mb-0"
>
<Space size={[0, 8]} nowrap>
{category.map((cat, index) => (
<Tag
size="small"
key={`${cat}-${index}`}
color="blue"
className="px-2 py-1 rounded-md"
>
{cat}
</Tag>
))}
</Space>
</Paragraph>
);
},
},
{ {
title: "描述", title: "描述",
dataIndex: ["attributes", "description"], dataIndex: ["attributes", "description"],
key: "description", key: "description",
width: 300,
render: (text) => (
<Paragraph
ellipsis={{
rows: 2,
tooltip: text
}}
className="mb-0"
>
{text}
</Paragraph>
)
}, },
{ {
title: "总金额", title: "总金额",
@@ -118,7 +260,7 @@ const ServicePage = () => {
key: "totalAmount", key: "totalAmount",
className: "min-w-[150px] text-right", className: "min-w-[150px] text-right",
render: (amount) => ( render: (amount) => (
<span className="text-blue-600 font-medium"> <span style={{ color: '#f50', fontWeight: 'bold' }}>
¥ ¥
{amount?.toLocaleString("zh-CN", { {amount?.toLocaleString("zh-CN", {
minimumFractionDigits: 2, minimumFractionDigits: 2,
@@ -130,17 +272,40 @@ const ServicePage = () => {
{ {
title: "操作", title: "操作",
key: "action", key: "action",
fixed: 'right',
width: 220,
render: (_, record) => ( render: (_, record) => (
<Space> <Space onClick={e => e.stopPropagation()}>
<Button <Button
type="link" type="link"
icon={<EyeOutlined />} icon={<EyeOutlined />}
onClick={() => onClick={() => navigate(`/company/serviceTemplateInfo/${record.id}`)}
navigate(`/company/serviceTemplateInfo/${record.id}`)
}
> >
查看 查看
</Button> </Button>
<Button
type="link"
icon={<EditOutlined />}
onClick={() => navigate(`/company/serviceTemplateInfo/${record.id}?edit=true`)}
>
编辑
</Button>
<Popconfirm
title="删除确认"
description="确定要删除这个服务模板吗?此操作不可恢复。"
okText="确定"
cancelText="取消"
okButtonProps={{ danger: true }}
onConfirm={() => handleDeleteService(record.id)}
>
<Button
type="link"
danger
className="text-red-600"
>
删除
</Button>
</Popconfirm>
</Space> </Space>
), ),
}, },
@@ -152,28 +317,71 @@ const ServicePage = () => {
title: "项目名称", title: "项目名称",
dataIndex: "name", dataIndex: "name",
key: "name", key: "name",
width: "40%", width: "25%",
render: (text, record) => { render: (text, record) => {
const isEditing = record.key === editingKey; const isEditing = record.key === editingKey;
return isEditing ? ( return isEditing ? (
<Input <Form.Item
value={text} key={`name-${record.key}`}
onChange={e => { name={[record.sectionIndex, "items", record.itemIndex, "name"]}
const newData = [...data]; className="!mb-0"
const targetService = newData.find(item => item.id === record.serviceId); >
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex]; <Input
targetItem.name = e.target.value; value={text}
setData(newData); onChange={e => {
}} const newData = [...data];
/> const targetService = newData.find(item => item.id === record.serviceId);
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
targetItem.name = e.target.value;
setData(newData);
}}
/>
</Form.Item>
) : text; ) : text;
} }
}, },
{
title: "描述",
dataIndex: "description",
key: "description",
width: "25%",
render: (text, record) => {
const isEditing = record.key === editingKey;
return isEditing ? (
<Form.Item
name={[record.sectionIndex, "items", record.itemIndex, "description"]}
className="!mb-0"
>
<Input.TextArea
rows={1}
value={text}
onChange={e => {
const newData = [...data];
const targetService = newData.find(item => item.id === record.serviceId);
const targetItem = targetService.attributes.sections[record.sectionIndex].items[record.itemIndex];
targetItem.description = e.target.value;
setData(newData);
}}
/>
</Form.Item>
) : (
<Paragraph
ellipsis={{
rows: 2,
tooltip: text
}}
className="mb-0"
>
{text}
</Paragraph>
);
}
},
{ {
title: "单价", title: "单价",
dataIndex: "price", dataIndex: "price",
key: "price", key: "price",
width: "20%", width: "15%",
render: (price, record) => { render: (price, record) => {
const isEditing = record.key === editingKey; const isEditing = record.key === editingKey;
return isEditing ? ( return isEditing ? (
@@ -202,7 +410,7 @@ const ServicePage = () => {
title: "数量", title: "数量",
dataIndex: "quantity", dataIndex: "quantity",
key: "quantity", key: "quantity",
width: "20%", width: "15%",
render: (quantity, record) => { render: (quantity, record) => {
const isEditing = record.key === editingKey; const isEditing = record.key === editingKey;
return isEditing ? ( return isEditing ? (
@@ -296,21 +504,24 @@ const ServicePage = () => {
} }
]; ];
// 修改 expandedRowRender 以支持编辑 // 修改 expandedRowRender 函数,确保 key 的唯一性
const expandedRowRender = (record) => ( const expandedRowRender = (record) => (
<div className="bg-gray-50 p-4 rounded-lg"> <div className="bg-white dark:bg-gray-800 p-4 rounded-lg">
{record.attributes.sections?.map((section, sectionIndex) => ( {record.attributes.sections?.map((section, sectionIndex) => (
<Card <Card
key={sectionIndex} key={`${record.id}-section-${sectionIndex}`}
className="mb-4 shadow-sm" className="mb-4 shadow-sm border-blue-100 dark:border-blue-900/30 hover:shadow-md transition-shadow duration-300"
title={ title={
<div className="flex items-center justify-between"> <div className="flex items-center justify-between py-2">
<span className="text-lg font-medium">{section.sectionName}</span> <span className="text-lg font-medium text-blue-600 dark:text-blue-400">
<span className="text-blue-600 font-medium"> {section.sectionName || `服务类型 ${sectionIndex + 1}`}
</span>
<span className="text-blue-600 dark:text-blue-400 font-medium">
总计: ¥ 总计: ¥
{section.items {section.items
.reduce( .reduce(
(total, item) => total + item.price * item.quantity, (total, item) =>
total + (Number(item.price) || 0) * (Number(item.quantity) || 0),
0 0
) )
.toLocaleString("zh-CN", { .toLocaleString("zh-CN", {
@@ -320,18 +531,26 @@ const ServicePage = () => {
</span> </span>
</div> </div>
} }
headStyle={{
background: 'rgba(59, 130, 246, 0.05)',
borderBottom: '1px solid rgba(59, 130, 246, 0.1)'
}}
bodyStyle={{
background: 'rgba(59, 130, 246, 0.02)'
}}
> >
<Table <Table
columns={itemColumns} columns={itemColumns}
dataSource={section.items.map((item, itemIndex) => ({ dataSource={section.items.map((item, itemIndex) => ({
...item, ...item,
key: `${sectionIndex}-${itemIndex}`, key: `${record.id}-${sectionIndex}-${itemIndex}`,
serviceId: record.id, serviceId: record.id,
sectionIndex: sectionIndex, sectionIndex,
itemIndex: itemIndex itemIndex
}))} }))}
pagination={false} pagination={false}
className="rounded-lg overflow-hidden" className="rounded-lg overflow-hidden"
rowClassName="hover:bg-blue-50/50 dark:hover:bg-blue-900/20 transition-colors"
/> />
</Card> </Card>
))} ))}
@@ -343,10 +562,10 @@ const ServicePage = () => {
}, []); }, []);
return ( return (
<div className="min-h-screen bg-gray-100 "> <div className="min-h-screen">
<Card <Card
title={<span className="text-xl font-medium">服务模版管理</span>} title={<span className="text-xl font-medium">服务模版管理</span>}
className="shadow-lg rounded-lg" className="h-full w-full overflow-auto"
extra={ extra={
<Button <Button
type="primary" type="primary"
@@ -361,11 +580,36 @@ const ServicePage = () => {
dataSource={data} dataSource={data}
loading={loading} loading={loading}
rowKey="id" rowKey="id"
scroll={{ x: true }}
expandable={{ expandable={{
expandedRowRender, expandedRowRender,
expandRowByClick: true,
expandIcon: ({ expanded, onExpand, record }) => (
<Button
type="link"
onClick={e => {
e.stopPropagation();
onExpand(record, e);
}}
icon={expanded ? <UpOutlined /> : <DownOutlined />}
className={`
transition-all duration-300
${expanded
? 'text-blue-600'
: 'text-gray-400 hover:text-blue-600'
}
`}
/>
),
rowExpandable: record => record.attributes.sections?.length > 0,
}} }}
className="rounded-lg overflow-hidden" className="rounded-lg overflow-hidden"
rowClassName="hover:bg-gray-50 transition-colors" rowClassName={(record, index) => `
cursor-pointer transition-colors
${index % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-750'}
hover:bg-blue-50 dark:hover:bg-blue-900/20
`}
expandedRowClassName={() => 'bg-blue-50/50 dark:bg-blue-900/10'}
/> />
</Card> </Card>
</div> </div>