Merge branch 'main' of github.com:xuqssq/uppmkt-admin

This commit is contained in:
‘Liammcl’
2024-12-28 19:24:06 +08:00
12 changed files with 259 additions and 175 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,18 +1,24 @@
import React from 'react';
import { RocketOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
import React from "react";
const { Text } = Typography;
import logo from "@/assets/logo.png";
import logoCollapsed from "@/assets/logo-collapsed.png";
export const Logo = ({ collapsed, isDarkMode }) => (
<div className="logo">
<div className="flex items-center justify-center gap-2">
<RocketOutlined className="text-2xl text-primary-500" />
{!collapsed && (
<Text className="text-lg font-semibold m-0" style={{ color: 'var(--primary-color)' }}>
Uppeta
</Text>
{collapsed ? (
<div className="flex items-center justify-center">
<img
src={logoCollapsed}
alt="logo"
className="w-1/2 object-contain"
/>
</div>
) : (
<div className="flex items-center justify-center">
<img src={logo} alt="logo" className="w-1/2 object-contain mr-2" />
</div>
)}
</div>
</div>
);
);

View File

@@ -15,7 +15,7 @@ const MainLayout = () => {
<Layout className="h-screen overflow-hidden">
<Sidebar collapsed={collapsed} />
<Layout className="flex flex-col ">
<Header collapsed={collapsed} setCollapsed={setCollapsed} />
<Header collapsed={collapsed} setCollapsed={setCollapsed}/>
<Content
className={`
m-2 p-4 rounded-lg overflow-auto h-full

View File

@@ -617,6 +617,7 @@ const SectionList = ({
max={100}
value={taxRate}
onChange={(value) => setTaxRate(value)}
disabled={isView}
/>
</div>
</div>

View File

@@ -143,13 +143,15 @@ const CustomerPage = () => {
}
className='h-full w-full overflow-auto'
extra={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate('/company/customerInfo')}
>
新增客户
</Button>
<div className="flex justify-between my-4">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/company/customerInfo")}
>
新增客户
</Button>
</div>
}
>
<Table

View File

@@ -1,20 +1,50 @@
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'
import { formatExchangeRate,EXCHANGE_RATE,defaultSymbol } from '@/utils/exchange_rate';
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";
import {
formatExchangeRate,
EXCHANGE_RATE,
defaultSymbol,
} from "@/utils/exchange_rate";
const QuotationPage = () => {
const navigate = useNavigate();
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
const [sorter, setSorter] = useState({ field: 'created_at', order: 'descend' });
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 [selectedCategory, setSelectedCategory] = useState("all");
const [categories, setCategories] = useState([]);
const {
@@ -22,8 +52,8 @@ const QuotationPage = () => {
loading: loadingQuotations,
total,
fetchResources: fetchQuotations,
deleteResource: deleteQuotation
} = useResources(pagination, sorter, 'quota');
deleteResource: deleteQuotation,
} = useResources(pagination, sorter, "quota");
useEffect(() => {
fetchQuotations();
@@ -43,10 +73,10 @@ const QuotationPage = () => {
const handleDelete = async (id) => {
try {
await deleteQuotation(id);
message.success('删除成功');
message.success("删除成功");
fetchQuotations();
} catch (error) {
message.error('删除失败:' + error.message);
message.error("删除失败:" + error.message);
}
};
@@ -89,7 +119,7 @@ const QuotationPage = () => {
if (selectedTemplateId) {
navigate(`/company/quotaInfo?templateId=${selectedTemplateId}`);
} else {
navigate('/company/quotaInfo');
navigate("/company/quotaInfo");
}
setIsModalVisible(false);
setSelectedTemplateId(null);
@@ -97,41 +127,41 @@ const QuotationPage = () => {
const getAllCategories = (templates) => {
const categorySet = new Set();
templates.forEach(template => {
template.attributes.category?.forEach(cat => {
templates.forEach((template) => {
template.attributes.category?.forEach((cat) => {
categorySet.add(JSON.stringify(cat));
});
});
return Array.from(categorySet).map(cat => JSON.parse(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)
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',
title: "报价单名称",
dataIndex: ["attributes", "quataName"],
key: "quataName",
ellipsis: true,
},
{
title: '客户信息',
dataIndex: ['attributes', 'customers'],
key: 'customers',
title: "客户信息",
dataIndex: ["attributes", "customers"],
key: "customers",
render: (customers, record) => (
<Space>
{customers?.map(customer => (
{customers?.map((customer) => (
<Tag
key={customer.id}
color="blue"
className='cursor-pointer'
className="cursor-pointer"
onClick={() => {
navigate(`/company/customerInfo/${customer.id}`)
navigate(`/company/customerInfo/${customer.id}`);
}}
>
{customer.name}
@@ -141,61 +171,70 @@ const QuotationPage = () => {
),
},
{
title: '报价总额',
dataIndex: ['attributes'],
key: 'totalAmount',
align: 'right',
title: "报价总额",
dataIndex: ["attributes"],
key: "totalAmount",
align: "right",
render: (attributes) => {
// 获取货币符号
const currencySymbol = EXCHANGE_RATE[attributes?.currency]?.symbol || defaultSymbol;
const currencySymbol =
EXCHANGE_RATE[attributes?.currency]?.symbol || defaultSymbol;
return (
<div className="space-y-1">
<div className="flex justify-between items-center text-sm">
<Typography.Text type="secondary" style={{ fontSize: '12px' }}>
税前{formatExchangeRate(attributes?.currency, attributes.beforeTaxAmount)}
<Typography.Text type="secondary" style={{ fontSize: "12px" }}>
税前
{formatExchangeRate(
attributes?.currency,
attributes.beforeTaxAmount
)}
</Typography.Text>
</div>
<div className="flex justify-between items-center">
<Statistic
value={formatExchangeRate(attributes?.currency, attributes.afterTaxAmount)}
value={formatExchangeRate(
attributes?.currency,
attributes.afterTaxAmount
)}
precision={2}
valueStyle={{
fontSize: '16px',
fontSize: "16px",
fontWeight: 600,
color: '#1890ff'
color: "#1890ff",
}}
className="!mb-0"
/>
</div>
</div>
);
},
},
{
title: '创建日期',
dataIndex: 'created_at',
key: 'created_at',
title: "创建日期",
dataIndex: "created_at",
key: "created_at",
sorter: true,
render: (text) => (
<span>{new Date(text).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}</span>
<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',
fixed: 'right',
title: "操作",
key: "action",
fixed: "right",
render: (_, record) => (
<Space size={0}>
<Button
size='small'
size="small"
type="link"
icon={<CopyOutlined />}
onClick={() => copyItem(record)}
@@ -203,7 +242,7 @@ const QuotationPage = () => {
复制
</Button>
<Button
size='small'
size="small"
type="link"
icon={<EyeOutlined />}
onClick={() => navigate(`/company/quotaInfo/preview/${record.id}`)}
@@ -211,10 +250,12 @@ const QuotationPage = () => {
查看
</Button>
<Button
size='small'
size="small"
type="link"
icon={<EditOutlined />}
onClick={() => navigate(`/company/quotaInfo/${record.id}?edit=true`)}
onClick={() =>
navigate(`/company/quotaInfo/${record.id}?edit=true`)
}
>
编辑
</Button>
@@ -226,7 +267,7 @@ const QuotationPage = () => {
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button size='small' type="link" danger icon={<DeleteOutlined />}>
<Button size="small" type="link" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
@@ -239,19 +280,21 @@ const QuotationPage = () => {
const groups = new Map();
// 添加未分类组
groups.set('uncategorized', {
name: '未分类',
templates: templates.filter(t => !t.attributes.category || t.attributes.category.length === 0)
groups.set("uncategorized", {
name: "未分类",
templates: templates.filter(
(t) => !t.attributes.category || t.attributes.category.length === 0
),
});
// 按分类分组
templates.forEach(template => {
templates.forEach((template) => {
if (template.attributes.category) {
template.attributes.category.forEach(cat => {
template.attributes.category.forEach((cat) => {
if (!groups.has(cat.id)) {
groups.set(cat.id, {
name: cat.name,
templates: []
templates: [],
});
}
groups.get(cat.id).templates.push(template);
@@ -259,35 +302,35 @@ const QuotationPage = () => {
}
});
return Array.from(groups.values()).filter(group => group.templates.length > 0);
return Array.from(groups.values()).filter(
(group) => group.templates.length > 0
);
};
const copyItem = async (record) => {
try {
setLoading(true);
// 深拷贝原有数据的 attributes
const newAttributes = JSON.parse(JSON.stringify(record.attributes));
// 修改报价单名称,添加"副本"标识
newAttributes.quataName = `${newAttributes.quataName} (副本)`;
// 创建新的报价单记录
const { data, error } = await supabase
.from('resources')
.insert([
{
type: 'quota',
attributes: newAttributes
}
]);
const { data, error } = await supabase.from("resources").insert([
{
type: "quota",
attributes: newAttributes,
},
]);
if (error) throw error;
message.success('复制成功');
message.success("复制成功");
// 刷新列表
fetchQuotations();
} catch (error) {
console.error('复制报价单失败:', error);
message.error('复制失败:' + error.message);
console.error("复制报价单失败:", error);
message.error("复制失败:" + error.message);
} finally {
setLoading(false);
}
@@ -301,19 +344,21 @@ const QuotationPage = () => {
<Tag color="blue">{total} 个报价单</Tag>
</Space>
}
className='h-full w-full overflow-auto'
className="h-full w-full overflow-auto"
extra={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsModalVisible(true)}
>
新增报价单
</Button>
<div className="flex justify-between my-4">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIsModalVisible(true)}
>
新增报价单
</Button>
</div>
}
>
<Table
className='w-full'
className="w-full"
columns={columns}
dataSource={quotations}
rowKey="id"
@@ -373,7 +418,7 @@ const QuotationPage = () => {
</div>
<div className="grid grid-cols-3 gap-4">
{group.templates.map(template => (
{group.templates.map((template) => (
<div
key={template.id}
onClick={() => handleTemplateSelect(template.id)}
@@ -381,8 +426,8 @@ const QuotationPage = () => {
relative p-4 rounded-xl cursor-pointer transition-all duration-200
${
selectedTemplateId === template.id
? 'ring-2 ring-blue-500 bg-blue-50/40 dark:bg-blue-900/40'
: 'hover:bg-gray-50 dark:hover:bg-gray-700/50 border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md'
? "ring-2 ring-blue-500 bg-blue-50/40 dark:bg-blue-900/40"
: "hover:bg-gray-50 dark:hover:bg-gray-700/50 border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md"
}
dark:bg-gray-800
`}
@@ -393,7 +438,7 @@ const QuotationPage = () => {
{template.attributes.templateName}
</h4>
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
{template.attributes.description || '暂无描述'}
{template.attributes.description || "暂无描述"}
</p>
</div>
<div className="text-blue-600 dark:text-blue-400 font-medium whitespace-nowrap">
@@ -422,7 +467,11 @@ const QuotationPage = () => {
{selectedTemplateId === template.id && (
<div className="absolute top-3 right-3">
<div className="w-5 h-5 bg-blue-500 dark:bg-blue-600 rounded-full flex items-center justify-center">
<svg className="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<svg
className="w-3 h-3 text-white"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
</svg>
</div>
@@ -440,4 +489,4 @@ const QuotationPage = () => {
);
};
export default QuotationPage;
export default QuotationPage;

View File

@@ -1,21 +1,24 @@
import React, { useEffect, useState } from 'react';
import { Card, Table, Button, message, Popconfirm, Tag, Space } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { useResources } from '@/hooks/resource/useResource';
import { useNavigate } from 'react-router-dom';
import React, { useEffect, useState } from "react";
import { Card, Table, Button, message, Popconfirm, Tag, Space } from "antd";
import { PlusOutlined, EditOutlined, DeleteOutlined } from "@ant-design/icons";
import { useResources } from "@/hooks/resource/useResource";
import { useNavigate } from "react-router-dom";
const SupplierPage = () => {
const navigate = useNavigate();
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
const [sorter, setSorter] = useState({ field: 'created_at', order: 'descend' });
const [sorter, setSorter] = useState({
field: "created_at",
order: "descend",
});
const {
resources: suppliers,
loading,
total,
fetchResources: fetchSuppliers,
deleteResource: deleteSupplier
} = useResources(pagination, sorter, 'supplier');
deleteResource: deleteSupplier,
} = useResources(pagination, sorter, "supplier");
useEffect(() => {
fetchSuppliers();
@@ -35,66 +38,70 @@ const SupplierPage = () => {
const handleDelete = async (id) => {
try {
await deleteSupplier(id);
message.success('删除成功');
message.success("删除成功");
fetchSuppliers();
} catch (error) {
message.error('删除失败:' + error.message);
message.error("删除失败:" + error.message);
}
};
const columns = [
{
title: '供应商名称',
dataIndex: ['attributes', 'name'],
key: 'name',
title: "供应商名称",
dataIndex: ["attributes", "name"],
key: "name",
ellipsis: true,
},
{
title: '联系人',
dataIndex: ['attributes', 'contact'],
key: 'contact',
title: "联系人",
dataIndex: ["attributes", "contact"],
key: "contact",
},
{
title: '电话',
dataIndex: ['attributes', 'phone'],
key: 'phone',
title: "电话",
dataIndex: ["attributes", "phone"],
key: "phone",
},
{
title: '状态',
dataIndex: ['attributes', 'status'],
key: 'status',
title: "状态",
dataIndex: ["attributes", "status"],
key: "status",
render: (status) => (
<Tag color={status === 'active' ? 'green' : 'red'}>
{status === 'active' ? '启用' : '禁用'}
<Tag color={status === "active" ? "green" : "red"}>
{status === "active" ? "启用" : "禁用"}
</Tag>
),
},
{
title: '创建日期',
dataIndex: 'created_at',
key: 'created_at',
title: "创建日期",
dataIndex: "created_at",
key: "created_at",
sorter: true,
render: (text) => (
<span>{new Date(text).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})}</span>
<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',
fixed: 'right',
title: "操作",
key: "action",
fixed: "right",
render: (_, record) => (
<Space size={0}>
<Button
size='small'
size="small"
type="link"
icon={<EditOutlined />}
onClick={() => navigate(`/company/supplierInfo/${record.id}?edit=true`)}
onClick={() =>
navigate(`/company/supplierInfo/${record.id}?edit=true`)
}
>
编辑
</Button>
@@ -106,7 +113,7 @@ const SupplierPage = () => {
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button size='small' type="link" danger icon={<DeleteOutlined />}>
<Button size="small" type="link" danger icon={<DeleteOutlined />}>
删除
</Button>
</Popconfirm>
@@ -123,15 +130,17 @@ const SupplierPage = () => {
<Tag color="blue">{total} 个供应商</Tag>
</Space>
}
className='h-full w-full overflow-auto'
className="h-full w-full overflow-auto"
extra={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate('/company/supplierInfo')}
>
新增供应商
</Button>
<div className="flex justify-between my-4">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/company/supplierInfo")}
>
新增供应商
</Button>
</div>
}
>
<Table
@@ -153,4 +162,4 @@ const SupplierPage = () => {
);
};
export default SupplierPage;
export default SupplierPage;

View File

@@ -30,9 +30,11 @@ const TaskPage = () => {
<Card
title="任务管理"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增任务
</Button>
<div className="flex justify-between my-4">
<Button type="primary" icon={<PlusOutlined />}>
新增任务
</Button>
</div>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />

View File

@@ -35,9 +35,11 @@ const CommunicationTasks = () => {
<Card
title="沟通任务"
extra={
<Button type="primary" icon={<PlusOutlined />}>
新增任务
</Button>
<div className="flex justify-between my-4">
<Button type="primary" icon={<PlusOutlined />}>
新增任务
</Button>
</div>
}
>
<Table columns={columns} dataSource={[]} rowKey="id" />

View File

@@ -86,7 +86,7 @@ const ResourceTask = () => {
},
{
title: "操作",
fixed: 'right',
fixed: "right",
key: "action",
render: (_, record) => (
<Space size={0}>
@@ -136,13 +136,15 @@ const ResourceTask = () => {
}
className="h-full w-full overflow-auto"
extra={
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/resource/task/edit")}
>
新增任务
</Button>
<div className="flex justify-between my-4">
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => navigate("/resource/task/edit")}
>
新增任务
</Button>
</div>
}
>
<Table

View File

@@ -206,4 +206,15 @@ html.dark::view-transition-old(root) {
.ant-menu {
height: 100%;
border: none !important;
background-color: transparent !important;
}
.ant-layout-sider {
@apply rounded-r-xl dark:!bg-black/80
}
.ant-layout-header{
@apply rounded-b-sm ml-2
}