product management

This commit is contained in:
2025-10-09 08:37:51 +07:00
parent 479b57cd1e
commit 3a32b0fecc
12 changed files with 507 additions and 3 deletions

View File

@@ -6,6 +6,8 @@ import ProtectedAdmin from './pages/admin/auth/ProtectedAdmin'
import Auth from './pages/home/auth/Auth'
import UserManagement from './pages/admin/User/UserManagement'
import CategoryManagement from './pages/admin/Category/CategoryManagement'
import ProductManagement from './pages/admin/Product/ProductManagement'
import CreateProductForm from './pages/admin/Product/components/CreateProductForm'
export default function RouterSetup() {
return <Routes>
@@ -18,6 +20,9 @@ export default function RouterSetup() {
}>
<Route path='user' element={<UserManagement />}></Route>
<Route path='category' element={<CategoryManagement />}></Route>
<Route path='product' element={<ProductManagement />}>
<Route path='add' element={<CreateProductForm />}></Route>
</Route>
</Route>
</Routes>
}

View File

@@ -0,0 +1,23 @@
import axios from "axios"
export interface CreateProductDTO {
categorieId: string;
price: number;
iconUrl: string;
name: string;
des: string;
}
export const ProductApi = {
findAll: async () => {
let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/products`)
return result.data
},
create: async (data: CreateProductDTO) => {
let result = await axios.post(`${import.meta.env.VITE_SV_HOST}/products`, {
...data,
isActive: true
})
return result.data
}
}

View File

@@ -1,9 +1,11 @@
import { CategoryApi } from "./core/category.api";
import { CloudianryApi } from "./core/cloudinary.api";
import { ProductApi } from "./core/product.api";
import { UserApi } from "./core/user.api";
export const Apis = {
user: UserApi,
category: CategoryApi,
cloundInary: CloudianryApi
cloundInary: CloudianryApi,
product: ProductApi
}

View File

@@ -0,0 +1,136 @@
import React, { useEffect, useState } from 'react'
import type { Product } from '../../../types/product.type'
import { Apis } from '../../../apis'
import { Button, Space, Table } from 'antd'
import { Outlet, useNavigate } from 'react-router'
import { useSelector } from 'react-redux'
import type { StoreType } from '../../../stores'
import type { Category } from '../../../types/category.type'
export default function ProductManagement() {
const [categoryList, setCategoryList] = useState<Category[]>([])
const navigate = useNavigate()
const [productList, setProductList] = useState<Product[]>([])
const [selectProduct, setSelectProduct] = useState<Product>(null)
async function fetchProductList() {
try {
await Apis.category.getAll()
.then(res => {
setCategoryList(res)
})
let result = await Apis.product.findAll()
setProductList(result)
} catch (err) {
}
}
useEffect(() => {
fetchProductList()
}, [])
const columns = [
{
title: 'Số Thứ Tự',
key: 'index',
render: (_: any, __: any, index: number) => index + 1
},
{
title: 'Tên sản phẩm',
dataIndex: 'name',
key: 'name',
},
{
title: 'Giá sản phẩm',
dataIndex: 'price',
key: 'price',
},
{
title: 'Tên danh mục',
dataIndex: 'categoryId',
key: 'categoryId',
render: (_: any, record: Product) => (
<Space size="middle">
<span>{categoryList.find((item) => {
return item.id == record.categoryId
})?.title || "Unknow"}</span>
</Space>
)
},
{
title: 'Icon',
dataIndex: 'iconUrl',
key: 'iconUrl',
render: (_: any, record: Product) => (
<Space size="middle">
<img src={record.iconUrl} style={{
width: "50px",
height: "50px",
borderRadius: "50"
}} />
</Space>
)
},
{
title: 'Trạng Thái',
dataIndex: 'isActive',
key: 'isActive',
render: (_: any, record: Product) => (
<Space size="middle">
{
record.isActive ?
<Button color="danger" variant="solid">Ngừng Hoạt Đng</Button>
: <Button type='primary'>Kích Hoạt</Button>
}
</Space>
)
},
{
title: 'Mô Tả',
dataIndex: 'isActive',
key: 'isActive',
render: (_: any, record: Product) => (
<Space size="middle">
<Button onClick={() => {
setSelectProduct(record)
}}>Xem Tả</Button>
</Space>
)
},
{
title: 'Công Cụ',
dataIndex: 'isActive',
key: 'isActive',
render: (_: any, record: Product) => (
<Space size="middle">
<Button color="danger" variant="solid">Xóa</Button>
<Button type='primary'>Sửa</Button>
</Space>
)
}
];
return (
<div>
{
selectProduct && <div className='des_ox' dangerouslySetInnerHTML={{ __html: selectProduct.des }}></div>
}
<h1>ProductManagement</h1>
<Button onClick={() => {
navigate("add")
}} type='primary'>Add New Product</Button>
<Outlet></Outlet>
<Table
dataSource={productList}
columns={columns}
/>
</div>
)
}

View File

@@ -0,0 +1,149 @@
import React, { useEffect, useRef, useState } from 'react'
import { Editor } from '@tinymce/tinymce-react';
import type { Category } from '../../../../types/category.type';
import { Apis } from '../../../../apis';
import { Button, Form, Input, InputNumber, Select, Upload } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router';
import { useDispatch } from 'react-redux';
import { loadingAction } from '../../../../stores/slices/loading.slice';
export default function CreateProductForm() {
const dispatch = useDispatch()
const navigate = useNavigate()
const editorRef = useRef(null);
const [categoryList, setCategoryList] = useState<Category[]>([])
const [apiLoading, setApiLoading] = useState(false)
useEffect(() => {
Apis.category.getAll()
.then(res => {
setCategoryList(res)
})
}, [])
const [form] = Form.useForm()
const handleFinish = async (values: any) => {
try {
const file = values.iconUrl.file;
if (!file) {
alert("phải chọn hình")
return
}
let iconUrl = await Apis.cloundInary.upload(file)
const data = {
...values,
iconUrl: iconUrl,
des: editorRef.current?.getContent()
}
console.log('data', data)
let result = await Apis.product.create(data);
window.location.href = "/admin/product"
} catch (er) {
}
}
return (
<div style={{
border: "1px solid black"
}}>
<h1>Thêm sản phẩm</h1>
{
apiLoading ? <>Đang thêm ....</> : <>
<Form
form={form}
layout="vertical"
onFinish={handleFinish}
style={{ maxWidth: 500, margin: 'auto' }}
>
<Form.Item
label="Tên sản phẩm"
name="name"
rules={[{ required: true, message: 'Vui lòng nhập tên sản phẩm!' }]}
>
<Input placeholder="Nhập tên sản phẩm" />
</Form.Item>
<Form.Item
label="Giá sản phẩm"
name="price"
rules={[{ required: true, message: 'Vui lòng nhập giá sản phẩm!' }]}
>
<InputNumber
min={0}
style={{ width: '100%' }}
placeholder="Nhập giá sản phẩm"
/>
</Form.Item>
<Form.Item
label="Hình ảnh sản phẩm"
name="iconUrl"
valuePropName="file"
getValueFromEvent={(e) => e}
rules={[{ required: true, message: 'Vui lòng chọn hình ảnh!' }]}
>
<Upload
beforeUpload={() => false} // chặn upload tự động, chỉ lấy file
maxCount={1}
listType="picture"
>
<Button icon={<UploadOutlined />}>Chọn nh</Button>
</Upload>
</Form.Item>
<Form.Item
label="Danh mục"
name="categoryId"
rules={[{ required: true, message: 'Vui lòng chọn danh mục!' }]}
>
<Select placeholder="Chọn danh mục">
{categoryList.map((item) => (
<Select.Option key={item.id} value={item.id}>
{item.title}
</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block>
Thêm sản phẩm
</Button>
</Form.Item>
</Form>
<p> Tả Sản Phẩm</p>
<Editor
apiKey='gl284vy5pkwj6d6jdt9r7n6z7398b9gmowka4onfdqr1wq6h'
onInit={(_evt, editor) => editorRef.current = editor}
initialValue="<p>This is the initial content of the editor.</p>"
init={{
height: 500,
menubar: false,
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'code', 'help', 'wordcount'
],
toolbar: 'undo redo | blocks | ' +
'bold italic forecolor | alignleft aligncenter ' +
'alignright alignjustify | bullist numlist outdent indent | ' +
'removeformat | help',
content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }'
}}
/>
</>
}
</div>
)
}

View File

@@ -18,6 +18,11 @@ export default function Slider({ collapsed }: { collapsed: boolean }) {
key: 'category',
icon: <UserOutlined />,
label: "Quản lý danh mục",
},
{
key: 'product',
icon: <UserOutlined />,
label: "Quản lý sản phẩm",
}
]

View File

@@ -1,9 +1,11 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { userAction, userReducer } from "./slices/user.slice";
import { loadingReducer } from "./slices/loading.slice";
const RootReducer = combineReducers({
user: userReducer
user: userReducer,
loading: loadingReducer
})
export type StoreType = ReturnType<typeof RootReducer>

View File

@@ -0,0 +1,14 @@
import { createSlice } from "@reduxjs/toolkit";
const loadingSlice = createSlice({
name: "loading",
initialState: false,
reducers: {
change: (state) => {
state = !state
}
}
})
export const loadingReducer = loadingSlice.reducer
export const loadingAction = loadingSlice.actions

View File

@@ -0,0 +1,9 @@
export interface Product {
id: string;
categoryId: string;
price: number;
iconUrl: string;
isActive: boolean;
name: string;
des: string;
}