├── .gitignore ├── .yarnrc.yml ├── README.md ├── package-lock.json ├── package.json ├── public ├── access │ ├── icons8-profit-80.png │ ├── icons8-revenue-50.png │ └── icons8-sales-32.png ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── apis │ ├── axiosClient.ts │ └── handleAPI.ts ├── components │ ├── AddCategory.tsx │ ├── FilterProduct.tsx │ ├── FormItem.tsx │ ├── HeaderComponent.tsx │ ├── SalesAndPurchaseStatistic.tsx │ ├── SiderComponent.tsx │ ├── StatisticComponent.tsx │ ├── TableComponent.tsx │ ├── TopSellingAndLowQuantityStatictis.tsx │ └── index.ts ├── constants │ ├── appInfos.ts │ └── colors.ts ├── firebase │ └── firebaseConfig.ts ├── index.css ├── index.tsx ├── logo.svg ├── modals │ ├── AddPromotion.tsx │ ├── AddSubProductModal.tsx │ ├── ModalCategory.tsx │ ├── ModalExportData.tsx │ ├── ToogleSupplier.tsx │ └── index.ts ├── models │ ├── BillModel.ts │ ├── FormModel.ts │ ├── LogModel.ts │ ├── NotificationModel.ts │ ├── OrderModel.ts │ ├── Products.ts │ ├── PromotionModel.ts │ ├── SelectModel.ts │ ├── StatictisModel.ts │ └── SupplierModel.ts ├── react-app-env.d.ts ├── redux │ ├── reducers │ │ └── authReducer.ts │ └── store.ts ├── reportWebVitals.ts ├── routers │ ├── AuthRouter.tsx │ ├── MainRouter.tsx │ └── Routers.tsx ├── screens │ ├── Actions.tsx │ ├── HomeScreen.tsx │ ├── ManageStore.tsx │ ├── PromotionScreen.tsx │ ├── ReportScreen.tsx │ ├── Suppliers.tsx │ ├── auth │ │ ├── Login.tsx │ │ ├── SignUp.tsx │ │ └── components │ │ │ └── SocialLogin.tsx │ ├── bills │ │ └── index.tsx │ ├── categories │ │ ├── Categories.tsx │ │ └── CategoryDetail.tsx │ ├── index.ts │ ├── inventories │ │ ├── AddProduct.tsx │ │ ├── Inventories.tsx │ │ └── ProductDetail.tsx │ └── orther │ │ ├── AddOrder.tsx │ │ └── index.tsx ├── setupTests.ts └── utils │ ├── add0toNumber.ts │ ├── dateTime.ts │ ├── formatNumber.ts │ ├── getTreeValues.ts │ ├── handleCurrency.ts │ ├── handleExportExcel.ts │ ├── replaceName.ts │ └── uploadFile.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | .env 26 | 27 | .yarn 28 | .pnp.cjs 29 | .pnp.loader.mjs -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kanban", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ckeditor/ckeditor5-react": "^9.1.0", 7 | "@reduxjs/toolkit": "^2.2.7", 8 | "@testing-library/jest-dom": "^5.17.0", 9 | "@testing-library/react": "^13.4.0", 10 | "@testing-library/user-event": "^13.5.0", 11 | "@tinymce/tinymce-react": "^5.1.1", 12 | "@types/jest": "^27.5.2", 13 | "@types/node": "^16.18.105", 14 | "@types/react": "^18.3.4", 15 | "@types/react-dom": "^18.3.0", 16 | "antd": "^5.20.2", 17 | "axios": "^1.7.5", 18 | "chart.js": "^4.4.8", 19 | "ckeditor5": "^43.1.0", 20 | "ckeditor5-premium-features": "^43.1.0", 21 | "firebase": "^10.13.0", 22 | "iconsax-react": "^0.0.8", 23 | "query-string": "^9.1.0", 24 | "re-resizable": "^6.9.17", 25 | "react": "^18.3.1", 26 | "react-chartjs-2": "^5.3.0", 27 | "react-dom": "^18.3.1", 28 | "react-icons": "^5.3.0", 29 | "react-image-file-resizer": "^0.4.8", 30 | "react-redux": "^9.1.2", 31 | "react-router-dom": "^6.26.1", 32 | "react-scripts": "5.0.1", 33 | "redux": "^5.0.1", 34 | "typescript": "^4.9.5", 35 | "web-vitals": "^2.1.4", 36 | "xlsx": "^0.18.5" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject" 43 | }, 44 | "eslintConfig": { 45 | "extends": [ 46 | "react-app", 47 | "react-app/jest" 48 | ] 49 | }, 50 | "browserslist": { 51 | "production": [ 52 | ">0.2%", 53 | "not dead", 54 | "not op_mini all" 55 | ], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/access/icons8-profit-80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsdaoquang/kanban/62e9b8efa4728885a7ce681c881d2a0b27b631a2/public/access/icons8-profit-80.png -------------------------------------------------------------------------------- /public/access/icons8-revenue-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsdaoquang/kanban/62e9b8efa4728885a7ce681c881d2a0b27b631a2/public/access/icons8-revenue-50.png -------------------------------------------------------------------------------- /public/access/icons8-sales-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsdaoquang/kanban/62e9b8efa4728885a7ce681c881d2a0b27b631a2/public/access/icons8-sales-32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsdaoquang/kanban/62e9b8efa4728885a7ce681c881d2a0b27b631a2/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 22 | Kanban 23 | 24 | 25 | 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsdaoquang/kanban/62e9b8efa4728885a7ce681c881d2a0b27b631a2/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bsdaoquang/kanban/62e9b8efa4728885a7ce681c881d2a0b27b631a2/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | .ant-menu-title-content a:hover{ 41 | text-decoration: none; 42 | } 43 | 44 | .icon-wapper { 45 | width: 38px; 46 | height: 38px; 47 | border-radius: 12px; 48 | padding: 2px; 49 | display: flex; 50 | justify-content: center; 51 | align-items: center; 52 | } 53 | 54 | .ant-card-body { 55 | padding: 16px !important; 56 | } 57 | 58 | .ant-divider-horizontal{ 59 | margin: 0 0 16px 0 60 | } 61 | 62 | a{ 63 | text-decoration: none !important; 64 | } 65 | .text-2-line{ 66 | line-height: 1.5em; 67 | height: 1.5em; 68 | overflow: hidden; 69 | white-space: nowrap; 70 | text-overflow: ellipsis; 71 | width: 100%; 72 | } 73 | 74 | .filter-card{ 75 | -webkit-box-shadow: 0px 9px 29px -17px rgba(66, 68, 90, 1); 76 | -moz-box-shadow: 0px 9px 29px -17px rgba(66, 68, 90, 1); 77 | box-shadow: 0px 9px 29px -17px rgba(66, 68, 90, 1); 78 | } 79 | 80 | .ant-menu-item-selected::after{ 81 | border-bottom-width: 0px 82 | } -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ConfigProvider, message } from 'antd'; 4 | import Routers from './routers/Routers'; 5 | import { Provider } from 'react-redux'; 6 | import store from './redux/store'; 7 | import './App.css'; 8 | 9 | message.config({ 10 | top: 20, 11 | duration: 2, 12 | maxCount: 3, 13 | rtl: true, 14 | prefixCls: 'my-message', 15 | }); 16 | 17 | function App() { 18 | return ( 19 | <> 20 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | 33 | export default App; 34 | -------------------------------------------------------------------------------- /src/apis/axiosClient.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import axios from 'axios'; 4 | import queryString from 'query-string'; 5 | import { localDataNames } from '../constants/appInfos'; 6 | 7 | const baseURL = `http://localhost:3002`; 8 | const baseURLProduction = `https://server-kanban.onrender.com`; 9 | 10 | const getAssetToken = () => { 11 | const res = localStorage.getItem(localDataNames.authData); 12 | 13 | if (res) { 14 | const auth = JSON.parse(res); 15 | return auth && auth.token ? auth.token : ''; 16 | } else { 17 | return ''; 18 | } 19 | }; 20 | 21 | const axiosClient = axios.create({ 22 | baseURL: baseURL, 23 | paramsSerializer: (params) => queryString.stringify(params), 24 | }); 25 | 26 | axiosClient.interceptors.request.use(async (config: any) => { 27 | const accesstoken = getAssetToken(); 28 | 29 | config.headers = { 30 | Authorization: accesstoken ? `Bearer ${accesstoken}` : '', 31 | Accept: 'application/json', 32 | ...config.headers, 33 | }; 34 | 35 | return { ...config, data: config.data ?? null }; 36 | }); 37 | 38 | axiosClient.interceptors.response.use( 39 | (res) => { 40 | if (res.data && res.status >= 200 && res.status < 300) { 41 | return res.data; 42 | } else { 43 | return Promise.reject(res.data); 44 | } 45 | }, 46 | (error) => { 47 | const { response } = error; 48 | return Promise.reject(response.data); 49 | } 50 | ); 51 | 52 | export default axiosClient; 53 | -------------------------------------------------------------------------------- /src/apis/handleAPI.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import axiosClient from './axiosClient'; 4 | 5 | const handleAPI = async ( 6 | url: string, 7 | data?: any, 8 | method?: 'post' | 'put' | 'get' | 'delete' 9 | ) => { 10 | return await axiosClient(url, { 11 | method: method ?? 'get', 12 | data, 13 | }); 14 | }; 15 | export default handleAPI; 16 | -------------------------------------------------------------------------------- /src/components/AddCategory.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Button, Form, Input, message, Space, TreeSelect } from 'antd'; 4 | import { useEffect, useState } from 'react'; 5 | import { replaceName } from '../utils/replaceName'; 6 | import handleAPI from '../apis/handleAPI'; 7 | import { TreeModel } from '../models/FormModel'; 8 | import { CategoyModel } from '../models/Products'; 9 | 10 | interface Props { 11 | onAddNew: (val: any) => void; 12 | values: TreeModel[]; 13 | seleted?: CategoyModel; 14 | onClose?: () => void; 15 | } 16 | 17 | const AddCategory = (props: Props) => { 18 | const { values, onAddNew, seleted, onClose } = props; 19 | 20 | const [isLoading, setIsLoading] = useState(false); 21 | 22 | useEffect(() => { 23 | if (seleted) { 24 | form.setFieldsValue(seleted); 25 | } else { 26 | form.resetFields(); 27 | } 28 | }, [seleted]); 29 | 30 | const [form] = Form.useForm(); 31 | 32 | const handleCategory = async (values: any) => { 33 | const api = seleted 34 | ? `/products/update-category?id=${seleted._id}` 35 | : `/products/add-category`; 36 | const data: any = {}; 37 | 38 | for (const i in values) { 39 | data[i] = values[i] ?? ''; 40 | } 41 | 42 | data.slug = replaceName(values.title); 43 | 44 | try { 45 | const res = await handleAPI(api, data, seleted ? 'put' : 'post'); 46 | message.success('Add new category successfuly!!'); 47 | 48 | onAddNew(res.data); 49 | 50 | form.resetFields(); 51 | } catch (error: any) { 52 | message.error(error.message); 53 | } finally { 54 | setIsLoading(false); 55 | } 56 | }; 57 | 58 | return ( 59 | <> 60 |
66 | 67 | 73 | 74 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | 90 |
91 | 92 | {onClose && ( 93 | 102 | )} 103 | 110 | 111 |
112 | 113 | ); 114 | }; 115 | 116 | export default AddCategory; 117 | -------------------------------------------------------------------------------- /src/components/FilterProduct.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { 4 | Button, 5 | Card, 6 | Empty, 7 | Form, 8 | Select, 9 | Slider, 10 | Space, 11 | Spin, 12 | Typography, 13 | } from 'antd'; 14 | import { useEffect, useState } from 'react'; 15 | import handleAPI from '../apis/handleAPI'; 16 | import { SelectModel } from '../models/SelectModel'; 17 | import { colors } from '../constants/colors'; 18 | 19 | export interface FilterProductValue { 20 | color?: string; 21 | categories?: string[]; 22 | size?: string; 23 | price?: number[]; 24 | } 25 | 26 | interface Props { 27 | values: FilterProductValue; 28 | onFilter: (vals: FilterProductValue) => void; 29 | } 30 | 31 | const FilterProduct = (props: Props) => { 32 | const { values, onFilter } = props; 33 | 34 | const [isLoading, setIsLoading] = useState(false); 35 | const [selectDatas, setSelectDatas] = useState<{ 36 | categories: SelectModel[]; 37 | colors: string[]; 38 | prices: number[]; 39 | sizes: SelectModel[]; 40 | }>(); 41 | const [colorSelected, setColorSelected] = useState([]); 42 | 43 | const [form] = Form.useForm(); 44 | 45 | useEffect(() => { 46 | getData(); 47 | }, []); 48 | 49 | useEffect(() => { 50 | if ( 51 | selectDatas && 52 | selectDatas.categories && 53 | selectDatas.categories.length > 0 54 | ) { 55 | getFilterValues(); 56 | } 57 | }, [selectDatas?.categories]); 58 | 59 | const getData = async () => { 60 | setIsLoading(true); 61 | 62 | try { 63 | await getCategories(); 64 | } catch (error) { 65 | console.log(error); 66 | } finally { 67 | setIsLoading(false); 68 | } 69 | }; 70 | 71 | const getCategories = async () => { 72 | const res = await handleAPI(`/products/get-categories`); 73 | 74 | const data = 75 | res.data && res.data.length > 0 76 | ? res.data.map((item: any) => ({ 77 | label: item.title, 78 | value: item._id, 79 | })) 80 | : []; 81 | 82 | handleChangeValue('categories', data); 83 | }; 84 | 85 | const handleChangeValue = (key: string, val: any) => { 86 | const items: any = { ...selectDatas }; 87 | items[`${key}`] = val; 88 | 89 | setSelectDatas(items); 90 | }; 91 | 92 | const getFilterValues = async () => { 93 | const res = await handleAPI('/products/get-filter-values'); 94 | const items: any = { ...selectDatas }; 95 | 96 | const data: any = res.data; 97 | 98 | for (const i in data) { 99 | items[i] = data[i]; 100 | } 101 | 102 | setSelectDatas(items); 103 | }; 104 | 105 | const handleFilter = (values: any) => { 106 | onFilter({ ...values, colors: colorSelected }); 107 | }; 108 | 109 | return ( 110 | 117 | {isLoading ? ( 118 | 119 | ) : selectDatas ? ( 120 | <> 121 |
122 | 123 | 177 | 178 | {selectDatas.prices && selectDatas.prices.length > 0 && ( 179 | 180 | 185 | 186 | )} 187 |
188 | 189 |
190 | 193 |
194 | 195 | ) : ( 196 | 197 | )} 198 |
199 | ); 200 | }; 201 | 202 | export default FilterProduct; 203 | -------------------------------------------------------------------------------- /src/components/FormItem.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Checkbox, Form, Input, Select } from 'antd'; 4 | import React from 'react'; 5 | import { FormItemModel } from '../models/FormModel'; 6 | 7 | interface Props { 8 | item: FormItemModel; 9 | } 10 | 11 | const FormItem = (props: Props) => { 12 | const { item } = props; 13 | 14 | const renderInput = (item: FormItemModel) => { 15 | let content = <>; 16 | 17 | switch (item.type) { 18 | case 'checkbox': 19 | content = ; 20 | break; 21 | case 'select': 22 | content = 27 | ); 28 | break; 29 | } 30 | 31 | return content; 32 | }; 33 | 34 | return ( 35 | 45 | {renderInput(item)} 46 | 47 | ); 48 | }; 49 | 50 | export default FormItem; 51 | -------------------------------------------------------------------------------- /src/components/HeaderComponent.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { 4 | Avatar, 5 | Badge, 6 | Button, 7 | Drawer, 8 | Dropdown, 9 | Input, 10 | List, 11 | MenuProps, 12 | Space, 13 | Typography, 14 | } from 'antd'; 15 | import { Notification, SearchNormal1 } from 'iconsax-react'; 16 | import { colors } from '../constants/colors'; 17 | import { auth } from '../firebase/firebaseConfig'; 18 | import { useDispatch, useSelector } from 'react-redux'; 19 | import { authSeletor, removeAuth } from '../redux/reducers/authReducer'; 20 | import { Link, useNavigate } from 'react-router-dom'; 21 | import { signOut } from 'firebase/auth'; 22 | import handleAPI from '../apis/handleAPI'; 23 | import { useEffect, useState } from 'react'; 24 | import { NotificationModel } from '../models/NotificationModel'; 25 | import { DateTime } from '../utils/dateTime'; 26 | 27 | const HeaderComponent = () => { 28 | const [notifications, setNotifications] = useState([]); 29 | const [visibleModalNotification, setVisibleModalNotification] = 30 | useState(false); 31 | 32 | const user = useSelector(authSeletor); 33 | const dispatch = useDispatch(); 34 | const navigate = useNavigate(); 35 | const items: MenuProps['items'] = [ 36 | { 37 | key: 'logout', 38 | label: 'Đăng xuất', 39 | onClick: async () => { 40 | signOut(auth); 41 | dispatch(removeAuth({})); 42 | localStorage.clear(); 43 | 44 | navigate('/'); 45 | }, 46 | }, 47 | ]; 48 | 49 | useEffect(() => { 50 | getNotifications(); 51 | }, []); 52 | 53 | const getNotifications = async () => { 54 | const api = `/notifications`; 55 | 56 | try { 57 | const res = await handleAPI(api); 58 | setNotifications(res.data); 59 | } catch (error) { 60 | console.log(error); 61 | } 62 | }; 63 | 64 | const handleReadNotification = async (item: NotificationModel) => { 65 | if (!item.isRead) { 66 | const api = `/notifications/update?id=${item._id}`; 67 | try { 68 | const res = await handleAPI(api, { isRead: true }, 'put'); 69 | 70 | await getNotifications(); 71 | } catch (error) { 72 | console.log(error); 73 | } 74 | } 75 | setVisibleModalNotification(false); 76 | navigate(`/order?id=${item.id}`); 77 | }; 78 | 79 | return ( 80 |
81 |
82 | } 90 | /> 91 |
92 |
93 | 94 |
111 | 112 | setVisibleModalNotification(false)}> 116 | ( 119 | 120 | handleReadNotification(item)}> 123 | 125 | {item.title} 126 | 127 | 128 | } 129 | description={item.body} 130 | /> 131 | 132 | {DateTime.CalendarDate(item.createdAt)} 133 | 134 | 135 | )} 136 | /> 137 | 138 |
139 | ); 140 | }; 141 | 142 | export default HeaderComponent; 143 | -------------------------------------------------------------------------------- /src/components/SalesAndPurchaseStatistic.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Card, DatePicker, Dropdown, Empty, Radio, Spin } from 'antd'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { Bar, Line } from 'react-chartjs-2'; 6 | import handleAPI from '../apis/handleAPI'; 7 | import { DateTime } from '../utils/dateTime'; 8 | import { add0toNumber } from '../utils/add0toNumber'; 9 | 10 | const SalesAndPurchaseStatistic = () => { 11 | const [timeTypeSelected, setTimeTypeSelected] = useState('monthly'); 12 | const [isLoading, setIsLoading] = useState(false); 13 | const [datas, setDatas] = useState< 14 | { 15 | date: string; 16 | data: { 17 | orders: number; 18 | purchase: number; 19 | }; 20 | }[] 21 | >([]); 22 | 23 | const options = { 24 | responsive: true, 25 | plugins: { 26 | legend: { 27 | position: 'bottom' as const, 28 | }, 29 | title: { 30 | display: false, 31 | }, 32 | }, 33 | }; 34 | 35 | useEffect(() => { 36 | getSalseAndPurchase(); 37 | }, [timeTypeSelected]); 38 | 39 | const getSalseAndPurchase = async () => { 40 | const api = `/admin/order-purchase?timeType=${timeTypeSelected}`; 41 | try { 42 | const res = await handleAPI(api); 43 | setDatas(res.data); 44 | } catch (error) { 45 | console.log(error); 46 | } 47 | }; 48 | 49 | const renderChart = () => { 50 | return { 51 | labels: datas.map((item) => 52 | timeTypeSelected === 'yearly' 53 | ? `${add0toNumber(new Date(item.date as string).getMonth())}` 54 | : DateTime.getShortDate(item.date) 55 | ), 56 | datasets: [ 57 | { 58 | label: 'Sales', 59 | data: datas.map((item) => item.data.purchase), 60 | backgroundColor: 'rgba(255, 99, 132, 0.2)', 61 | borderColor: 'rgba(255, 99, 132, 1)', 62 | borderWidth: 1, 63 | }, 64 | { 65 | label: 'Orders', 66 | data: datas.map((item) => item.data.orders), 67 | backgroundColor: 'rgba(54, 162, 235, 0.2)', 68 | borderColor: 'rgba(54, 162, 235, 1)', 69 | borderWidth: 1, 70 | }, 71 | ], 72 | }; 73 | }; 74 | 75 | return isLoading ? ( 76 |
77 | 78 |
79 | ) : datas ? ( 80 |
81 |
82 | setTimeTypeSelected(e.target.value)} 88 | options={[ 89 | { label: 'Weekly', value: 'weekly' }, 90 | { 91 | label: 'Monthly', 92 | value: 'monthly', 93 | }, 94 | { 95 | label: 'Yearly', 96 | value: 'yearly', 97 | }, 98 | ]} 99 | optionType='button' 100 | /> 101 | }> 102 | 103 | 104 |
105 |
106 | 107 | 108 | 109 |
110 |
111 | ) : ( 112 | 113 | ); 114 | }; 115 | 116 | export default SalesAndPurchaseStatistic; 117 | -------------------------------------------------------------------------------- /src/components/SiderComponent.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Layout, Menu, MenuProps, Typography } from 'antd'; 4 | import { 5 | Box, 6 | Chart, 7 | DocumentCode, 8 | Home2, 9 | PercentageSquare, 10 | ProfileCircle, 11 | } from 'iconsax-react'; 12 | import { Link } from 'react-router-dom'; 13 | import { FiHome } from 'react-icons/fi'; 14 | import { CiViewList } from 'react-icons/ci'; 15 | import { MdOutlineInventory } from 'react-icons/md'; 16 | import { appInfo } from '../constants/appInfos'; 17 | import { colors } from '../constants/colors'; 18 | import { FaHistory, FaMoneyBill, FaTags } from 'react-icons/fa'; 19 | 20 | const { Sider } = Layout; 21 | const { Text } = Typography; 22 | type MenuItem = Required['items'][number]; 23 | 24 | const SiderComponent = () => { 25 | const items: MenuItem[] = [ 26 | { 27 | key: 'dashboard', 28 | label: Dashboard, 29 | icon: , 30 | }, 31 | { 32 | key: 'inventory', 33 | label: 'Inventory', 34 | icon: , 35 | children: [ 36 | { 37 | key: 'inventory', 38 | label: All, 39 | }, 40 | { 41 | key: 'addNew', 42 | label: Add new, 43 | }, 44 | ], 45 | }, 46 | { 47 | key: 'Categories', 48 | label: Categories, 49 | icon: , 50 | }, 51 | { 52 | key: 'Report', 53 | label: Report, 54 | icon: , 55 | }, 56 | { 57 | key: 'Suppliers', 58 | label: Suppliers, 59 | icon: , 60 | }, 61 | { 62 | key: 'Bills', 63 | label: Bills, 64 | icon: , 65 | }, 66 | { 67 | key: 'Orders', 68 | label: Orders, 69 | icon: , 70 | children: [ 71 | { 72 | key: 'all', 73 | label: All, 74 | }, 75 | { 76 | key: 'add', 77 | label: Add Order, 78 | }, 79 | ], 80 | }, 81 | { 82 | key: 'Manage Store', 83 | label: Manage Store, 84 | icon: , 85 | }, 86 | { 87 | key: 'Promotions', 88 | label: Promotions, 89 | icon: , 90 | }, 91 | { 92 | key: 'histories', 93 | label: Histories, 94 | icon: , 95 | }, 96 | ]; 97 | return ( 98 | 99 |
100 | 101 | 108 | {appInfo.title} 109 | 110 |
111 | 112 | 113 | ); 114 | }; 115 | 116 | export default SiderComponent; 117 | -------------------------------------------------------------------------------- /src/components/StatisticComponent.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Typography } from 'antd'; 4 | 5 | interface Props { 6 | value: string; 7 | title: string; 8 | color?: string; 9 | type?: 'vertical' | 'horizontal'; 10 | image?: string; 11 | } 12 | 13 | const StatisticComponent = (props: Props) => { 14 | const { value, title, color, type, image } = props; 15 | 16 | return ( 17 |
18 |
19 |
29 | 38 |
39 |
40 |
45 |
51 | 58 | {value} 59 | 60 |
61 | 68 | {title} 69 | 70 |
71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default StatisticComponent; 78 | -------------------------------------------------------------------------------- /src/components/TableComponent.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Button, Space, Table, Typography } from 'antd'; 4 | import { FormModel } from '../models/FormModel'; 5 | import { useEffect, useState } from 'react'; 6 | import { ColumnProps } from 'antd/es/table'; 7 | import { Sort } from 'iconsax-react'; 8 | import { colors } from '../constants/colors'; 9 | import { Resizable } from 're-resizable'; 10 | import { ModalExportData } from '../modals'; 11 | 12 | interface Props { 13 | forms: FormModel; 14 | loading?: boolean; 15 | records: any[]; 16 | onPageChange: (val: { page: number; pageSize: number }) => void; 17 | onAddNew: () => void; 18 | scrollHeight?: string; 19 | total: number; 20 | extraColumn?: (item: any) => void; 21 | api: string; 22 | } 23 | 24 | const { Title } = Typography; 25 | 26 | const TableComponent = (props: Props) => { 27 | const { 28 | forms, 29 | loading, 30 | records, 31 | onPageChange, 32 | onAddNew, 33 | total, 34 | scrollHeight, 35 | extraColumn, 36 | api, 37 | } = props; 38 | 39 | const [pageInfo, setPageInfo] = useState<{ 40 | page: number; 41 | pageSize: number; 42 | }>({ 43 | page: 1, 44 | pageSize: 10, 45 | }); 46 | const [columns, setColumns] = useState[]>([]); 47 | const [isVisibleModalExport, setIsVisibleModalExport] = useState(false); 48 | 49 | useEffect(() => { 50 | onPageChange(pageInfo); 51 | }, [pageInfo]); 52 | 53 | useEffect(() => { 54 | if (forms && forms.formItems && forms.formItems.length > 0) { 55 | const items: any[] = []; 56 | 57 | forms.formItems.forEach((item: any) => 58 | items.push({ 59 | key: item.key, 60 | dataIndex: item.value, 61 | title: item.label, 62 | width: item.displayLength, 63 | }) 64 | ); 65 | 66 | items.unshift({ 67 | key: 'index', 68 | dataIndex: 'index', 69 | title: '#', 70 | align: 'center', 71 | width: 100, 72 | }); 73 | 74 | if (extraColumn) { 75 | items.push({ 76 | key: 'actions', 77 | dataIndex: '', 78 | fixed: 'right', 79 | title: 'Action', 80 | align: 'right', 81 | render: (item: any) => extraColumn(item), 82 | width: 100, 83 | }); 84 | } 85 | 86 | setColumns(items); 87 | } 88 | }, [forms]); 89 | 90 | const RenderTitle = (props: any) => { 91 | const { children, ...restProps } = props; 92 | return ( 93 | 98 | { 101 | const item = columns.find( 102 | (element) => element.title === children[1] 103 | ); 104 | if (item) { 105 | const items = [...columns]; 106 | const newWidth = (item.width as number) + d.width; 107 | const index = columns.findIndex( 108 | (element) => element.key === item.key 109 | ); 110 | 111 | if (index !== -1) { 112 | items[index].width = newWidth; 113 | } 114 | 115 | setColumns(items); 116 | } 117 | }}> 118 | {children} 119 | 120 | 121 | ); 122 | }; 123 | 124 | return ( 125 | <> 126 | { 130 | setPageInfo({ ...pageInfo, pageSize: size }); 131 | }, 132 | total, 133 | onChange(page, pageSize) { 134 | setPageInfo({ 135 | ...pageInfo, 136 | page, 137 | }); 138 | }, 139 | showQuickJumper: true, 140 | }} 141 | scroll={{ 142 | y: scrollHeight ? scrollHeight : 'calc(100vh - 300px)', 143 | }} 144 | loading={loading} 145 | dataSource={records} 146 | columns={columns} 147 | bordered 148 | title={() => ( 149 |
150 |
151 | {forms.title} 152 |
153 |
154 | 155 | 158 | 161 | 164 | 165 |
166 |
167 | )} 168 | components={{ 169 | header: { 170 | cell: RenderTitle, 171 | }, 172 | }} 173 | /> 174 | setIsVisibleModalExport(false)} 177 | api={api} 178 | name={api} 179 | /> 180 | 181 | ); 182 | }; 183 | 184 | export default TableComponent; 185 | -------------------------------------------------------------------------------- /src/components/TopSellingAndLowQuantityStatictis.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Avatar, Button, Card, List, Table, Tag } from 'antd'; 4 | import { useEffect, useState } from 'react'; 5 | import { Link } from 'react-router-dom'; 6 | import handleAPI from '../apis/handleAPI'; 7 | import { ProductModel } from '../models/Products'; 8 | import { VND } from '../utils/handleCurrency'; 9 | 10 | const TopSellingAndLowQuantityStatictis = () => { 11 | const [isLoading, setIsLoading] = useState(false); 12 | const [datas, setDatas] = useState<{ 13 | topSelling: { 14 | product: ProductModel; 15 | total: number; 16 | count: number; 17 | qty: number; 18 | _id: string; 19 | }[]; 20 | lowQuantity: { 21 | _id: string; 22 | qty: number; 23 | images: string[]; 24 | title: string; 25 | }[]; 26 | }>(); 27 | 28 | useEffect(() => { 29 | getTopStatictis(); 30 | }, []); 31 | 32 | const getTopStatictis = async () => { 33 | const api = `/admin/top-selling`; 34 | setIsLoading(true); 35 | try { 36 | const res = await handleAPI(api); 37 | 38 | setDatas(res.data); 39 | } catch (error) { 40 | console.log(error); 41 | } finally { 42 | setIsLoading(false); 43 | } 44 | }; 45 | 46 | return ( 47 |
48 |
49 | See all}> 53 |
index + 1, 66 | }, 67 | { 68 | title: 'Name', 69 | dataIndex: 'product', 70 | key: 'product', 71 | render: (product: ProductModel) => product.title, 72 | }, 73 | { 74 | title: 'Sold', 75 | dataIndex: 'qty', 76 | key: 'qty', 77 | render: (value) => value.toLocaleString(), 78 | align: 'right', 79 | }, 80 | { 81 | title: 'Sell', 82 | dataIndex: 'count', 83 | key: 'count', 84 | render: (value) => value.toLocaleString(), 85 | align: 'right', 86 | }, 87 | { 88 | title: 'Total', 89 | dataIndex: 'total', 90 | key: 'total', 91 | render: (value) => VND.format(value), 92 | align: 'right', 93 | }, 94 | ]} 95 | /> 96 | 97 | 98 |
99 | See all}> 103 | ( 106 | Low}> 107 | 0 ? item.images[0] : ''} 111 | /> 112 | } 113 | title={item.title} 114 | description={`Quantity: ${item.qty}`} 115 | /> 116 | 117 | )} 118 | /> 119 | 120 |
121 | 122 | ); 123 | }; 124 | 125 | export default TopSellingAndLowQuantityStatictis; 126 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import AddCategory from './AddCategory'; 4 | import FilterProduct from './FilterProduct'; 5 | import HeaderComponent from './HeaderComponent'; 6 | import SalesAndPurchaseStatistic from './SalesAndPurchaseStatistic'; 7 | import SiderComponent from './SiderComponent'; 8 | import StatisticComponent from './StatisticComponent'; 9 | import TopSellingAndLowQuantityStatictis from './TopSellingAndLowQuantityStatictis'; 10 | 11 | export { 12 | HeaderComponent, 13 | SiderComponent, 14 | AddCategory, 15 | StatisticComponent, 16 | FilterProduct, 17 | SalesAndPurchaseStatistic, 18 | TopSellingAndLowQuantityStatictis, 19 | }; 20 | -------------------------------------------------------------------------------- /src/constants/appInfos.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export const localDataNames = { 4 | authData: 'authData', 5 | }; 6 | export const appInfo = { 7 | logo: 'https://firebasestorage.googleapis.com/v0/b/kanban-c0323.appspot.com/o/kanban-logo.png?alt=media&token=a3e8c386-57da-49a3-b9a2-94b8fd93ff83', 8 | title: 'KANBAN', 9 | description: '', 10 | }; 11 | -------------------------------------------------------------------------------- /src/constants/colors.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export const colors = { 4 | primary500: '#1570EF', 5 | gray600: '#5D6679', 6 | gray800: '#383E49', 7 | }; 8 | 9 | export const listColors = [ 10 | 'coral', 11 | 'magenta', 12 | 'red', 13 | 'volcano', 14 | 'orange', 15 | 'gold', 16 | 'lime', 17 | 'green', 18 | 'cyan', 19 | 'blue', 20 | 'geekblue', 21 | 'purple', 22 | ]; 23 | -------------------------------------------------------------------------------- /src/firebase/firebaseConfig.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import { initializeApp } from 'firebase/app'; 3 | import { getAuth } from 'firebase/auth'; 4 | import { getStorage } from 'firebase/storage'; 5 | 6 | const firebaseConfig = { 7 | apiKey: process.env.REACT_APP_apiKey, 8 | authDomain: process.env.REACT_APP_authDomain, 9 | projectId: process.env.REACT_APP_projectId, 10 | storageBucket: process.env.REACT_APP_storageBucket, 11 | messagingSenderId: process.env.REACT_APP_messagingSenderId, 12 | appId: process.env.REACT_APP_appId, 13 | }; 14 | 15 | const app = initializeApp(firebaseConfig); 16 | export const auth = getAuth(); 17 | export const storage = getStorage(); 18 | 19 | auth.languageCode = 'vi'; 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | .content-center{ 16 | height: 100vh; 17 | justify-content: center; 18 | align-items: center; 19 | display: flex; 20 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/client'; 5 | import './index.css'; 6 | import App from './App'; 7 | import reportWebVitals from './reportWebVitals'; 8 | 9 | const root = ReactDOM.createRoot( 10 | document.getElementById('root') as HTMLElement 11 | ); 12 | root.render( 13 | 14 | 15 | 16 | ); 17 | 18 | reportWebVitals(); 19 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modals/AddPromotion.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { 4 | DatePicker, 5 | Form, 6 | Input, 7 | message, 8 | Modal, 9 | Select, 10 | Upload, 11 | UploadProps, 12 | } from 'antd'; 13 | import { useEffect, useState } from 'react'; 14 | import handleAPI from '../apis/handleAPI'; 15 | import { uploadFile } from '../utils/uploadFile'; 16 | import { PromotionModel } from '../models/PromotionModel'; 17 | import dayjs from 'dayjs'; 18 | import { url } from 'inspector'; 19 | 20 | interface Props { 21 | visible: boolean; 22 | onClose: () => void; 23 | promotion?: PromotionModel; 24 | onAddNew: (val: PromotionModel) => void; 25 | } 26 | 27 | const AddPromotion = (props: Props) => { 28 | const { visible, onClose, promotion, onAddNew } = props; 29 | 30 | const [imageUpload, setImageUpload] = useState([]); 31 | const [isLoading, setIsLoading] = useState(false); 32 | 33 | const [form] = Form.useForm(); 34 | 35 | useEffect(() => { 36 | if (promotion) { 37 | form.setFieldsValue({ 38 | ...promotion, 39 | startAt: dayjs(promotion.startAt), 40 | endAt: dayjs(promotion.endAt), 41 | }); 42 | 43 | if (promotion.imageURL) { 44 | setImageUpload([ 45 | { uid: '-1', url: promotion.imageURL, status: 'done' }, 46 | ]); 47 | } 48 | } 49 | }, [promotion]); 50 | 51 | const handleClose = () => { 52 | form.resetFields(); 53 | onClose(); 54 | }; 55 | 56 | const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { 57 | const items = newFileList.map((item) => 58 | item.originFileObj 59 | ? { 60 | ...item, 61 | url: item.originFileObj 62 | ? URL.createObjectURL(item.originFileObj) 63 | : '', 64 | status: 'done', 65 | } 66 | : { ...item } 67 | ); 68 | 69 | setImageUpload(items); 70 | }; 71 | 72 | const handleAddNewPromotion = async (values: any) => { 73 | if (imageUpload.length === 0) { 74 | message.error('Please upload one image'); 75 | } else { 76 | const start = values.startAt; 77 | const end = values.endAt; 78 | 79 | if (new Date(end).getTime() < new Date(start).getTime()) { 80 | message.error('Thời gian kết thúc phải lớn thời gian bắt đầu'); 81 | } else { 82 | const data: any = {}; 83 | 84 | for (const i in values) { 85 | data[i] = values[i] ?? ''; 86 | } 87 | 88 | data.startAt = new Date(start); 89 | data.endAt = new Date(end); 90 | 91 | data.imageURL = 92 | imageUpload.length > 0 && imageUpload[0].originFileObj 93 | ? await uploadFile(imageUpload[0].originFileObj) 94 | : ''; 95 | 96 | const api = `/promotions/${ 97 | promotion ? `update?id=${promotion._id}` : 'add-new' 98 | }`; 99 | setIsLoading(true); 100 | 101 | try { 102 | const res = await handleAPI(api, data, promotion ? 'put' : 'post'); 103 | onAddNew(res.data); 104 | handleClose(); 105 | } catch (error) { 106 | console.log(error); 107 | } finally { 108 | setIsLoading(false); 109 | } 110 | } 111 | } 112 | }; 113 | return ( 114 | form.submit()}> 126 | 132 | {imageUpload.length === 0 ? 'Upload' : null} 133 | 134 |
140 | 144 | 145 | 146 | 147 | 148 | 149 |
150 |
151 | 152 | 153 | 154 |
155 |
156 | 157 | 158 | 159 |
160 |
161 |
162 |
163 | 164 | 165 | 166 |
167 |
168 | 169 | 182 | 183 | )} 184 | 185 | 186 | 187 | 188 | 197 | 198 | 199 | 200 |
201 |
202 | 203 | 204 | 205 |
206 |
207 | 208 | 209 | 210 |
211 | 212 |
213 | 214 | 215 | 216 |
217 |
218 | 219 | 220 | 221 | 222 | 228 | Upload 229 | 230 | 231 | {previewImage && ( 232 | setPreviewOpen(visible), 237 | afterOpenChange: (visible) => !visible && setPreviewImage(''), 238 | }} 239 | src={previewImage} 240 | /> 241 | )} 242 | 243 | ); 244 | }; 245 | 246 | export default AddSubProductModal; 247 | -------------------------------------------------------------------------------- /src/modals/ModalCategory.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Modal } from 'antd'; 4 | import { AddCategory } from '../components'; 5 | import { TreeModel } from '../models/FormModel'; 6 | 7 | interface Props { 8 | visible: boolean; 9 | onClose: () => void; 10 | onAddNew: (val: any) => void; 11 | values: TreeModel[]; 12 | } 13 | 14 | const ModalCategory = (props: Props) => { 15 | const { visible, onClose, onAddNew, values } = props; 16 | 17 | const handleClose = () => { 18 | onClose(); 19 | }; 20 | 21 | return ( 22 | 28 | { 31 | onAddNew(val); 32 | onClose(); 33 | }} 34 | /> 35 | 36 | ); 37 | }; 38 | 39 | export default ModalCategory; 40 | -------------------------------------------------------------------------------- /src/modals/ModalExportData.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { 4 | Button, 5 | Checkbox, 6 | DatePicker, 7 | Divider, 8 | List, 9 | message, 10 | Modal, 11 | Space, 12 | } from 'antd'; 13 | import React, { useEffect, useState } from 'react'; 14 | import { FormModel } from '../models/FormModel'; 15 | import handleAPI from '../apis/handleAPI'; 16 | import { DateTime } from '../utils/dateTime'; 17 | import { hanldExportExcel } from '../utils/handleExportExcel'; 18 | 19 | interface Props { 20 | visible: boolean; 21 | onClose: () => void; 22 | api: string; 23 | name?: string; 24 | } 25 | 26 | const { RangePicker } = DatePicker; 27 | 28 | const ModalExportData = (props: Props) => { 29 | const { visible, onClose, api, name } = props; 30 | 31 | const [isLoading, setIsLoading] = useState(false); 32 | const [isGetting, setIsGetting] = useState(false); 33 | const [forms, setForms] = useState(); 34 | const [checkedValues, setCheckedValues] = useState([]); 35 | const [timeSelected, setTimeSelected] = useState('ranger'); 36 | const [dates, setDates] = useState({ 37 | start: '', 38 | end: '', 39 | }); 40 | 41 | useEffect(() => { 42 | if (visible) { 43 | getFroms(); 44 | } 45 | }, [visible, api]); 46 | 47 | const getFroms = async () => { 48 | const url = `/${api}/get-form`; 49 | setIsGetting(true); 50 | try { 51 | const res = await handleAPI(url); 52 | res.data && setForms(res.data); 53 | } catch (error) { 54 | console.log(error); 55 | } finally { 56 | setIsGetting(false); 57 | } 58 | }; 59 | 60 | const handleChangeCheckedValue = (val: string) => { 61 | const items = [...checkedValues]; 62 | const index = items.findIndex((element) => element === val); 63 | 64 | if (index !== -1) { 65 | items.splice(index, 1); 66 | } else { 67 | items.push(val); 68 | } 69 | 70 | setCheckedValues(items); 71 | }; 72 | 73 | const handleExport = async () => { 74 | let url = ``; 75 | if (timeSelected !== 'all' && dates.start && dates.end) { 76 | if (new Date(dates.start).getTime() > new Date(dates.end).getTime()) { 77 | message.error('Thời gian lỗi!!!'); 78 | } else { 79 | url = `/${api}/get-export-data/?start=${dates.start}&end=${dates.end}`; 80 | } 81 | } else { 82 | url = `/${api}/get-export-data`; 83 | } 84 | 85 | const data = checkedValues; 86 | if (Object.keys(data).length > 0) { 87 | setIsLoading(true); 88 | try { 89 | const res = await handleAPI(url, data, 'post'); 90 | 91 | res.data && (await hanldExportExcel(res.data, api)); 92 | 93 | onClose(); 94 | } catch (error: any) { 95 | message.error(error.message); 96 | } finally { 97 | setIsLoading(false); 98 | } 99 | } else { 100 | message.error('Please selcte 1 key of values'); 101 | } 102 | }; 103 | 104 | return ( 105 | 115 |
116 |
117 | 120 | setTimeSelected(timeSelected === 'all' ? 'ranger' : 'all') 121 | }> 122 | Get all 123 | 124 |
125 |
126 | 129 | setTimeSelected(timeSelected === 'all' ? 'ranger' : 'all') 130 | }> 131 | Date ranger 132 | 133 |
134 |
135 | {timeSelected === 'ranger' && ( 136 | 137 | 139 | setDates( 140 | val && val[0] && val[1] 141 | ? { 142 | start: `${DateTime.CalendarDate(val[0])} 00:00:00`, 143 | end: `${DateTime.CalendarDate(val[1])} 00:00:00`, 144 | } 145 | : { 146 | start: '', 147 | end: '', 148 | } 149 | ) 150 | } 151 | /> 152 | 153 | )} 154 |
155 |
156 | 157 |
158 | ( 161 | 162 | handleChangeCheckedValue(item.value)}> 165 | {item.label} 166 | 167 | 168 | )} 169 | /> 170 |
171 |
172 | ); 173 | }; 174 | 175 | export default ModalExportData; 176 | -------------------------------------------------------------------------------- /src/modals/ToogleSupplier.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Avatar, Button, Form, message, Modal, Typography } from 'antd'; 4 | import { User } from 'iconsax-react'; 5 | import { useEffect, useRef, useState } from 'react'; 6 | import handleAPI from '../apis/handleAPI'; 7 | import FormItem from '../components/FormItem'; 8 | import { colors } from '../constants/colors'; 9 | import { FormModel } from '../models/FormModel'; 10 | import { SupplierModel } from '../models/SupplierModel'; 11 | import { replaceName } from '../utils/replaceName'; 12 | import { uploadFile } from '../utils/uploadFile'; 13 | 14 | const { Paragraph } = Typography; 15 | 16 | interface Props { 17 | visible: boolean; 18 | onClose: () => void; 19 | onAddNew: (val: SupplierModel) => void; 20 | supplier?: SupplierModel; 21 | } 22 | 23 | const ToogleSupplier = (props: Props) => { 24 | const { visible, onAddNew, onClose, supplier } = props; 25 | 26 | const [isLoading, setIsLoading] = useState(false); 27 | const [isGetting, setIsGetting] = useState(false); 28 | const [isTaking, setIsTaking] = useState(); 29 | const [formData, setFormData] = useState(); 30 | const [file, setFile] = useState(); 31 | 32 | const [form] = Form.useForm(); 33 | const inpRef = useRef(); 34 | 35 | useEffect(() => { 36 | getFormData(); 37 | }, []); 38 | 39 | useEffect(() => { 40 | if (supplier) { 41 | form.setFieldsValue(supplier); 42 | setIsTaking(supplier.isTaking === 1); 43 | } 44 | }, [supplier]); 45 | 46 | const addNewSupplier = async (values: any) => { 47 | setIsLoading(true); 48 | 49 | const data: any = {}; 50 | const api = `/supplier/${ 51 | supplier ? `update?id=${supplier._id}` : 'add-new' 52 | }`; 53 | 54 | for (const i in values) { 55 | data[i] = values[i] ?? ''; 56 | } 57 | 58 | data.price = values.price ? parseInt(values.price) : 0; 59 | data.isTaking = isTaking ? 1 : 0; 60 | 61 | if (file) { 62 | data.photoUrl = await uploadFile(file); 63 | } 64 | 65 | data.slug = replaceName(values.name); 66 | 67 | try { 68 | const res: any = await handleAPI(api, data, supplier ? 'put' : 'post'); 69 | message.success(res.message); 70 | !supplier && onAddNew(res.data); 71 | handleClose(); 72 | } catch (error) { 73 | console.log(error); 74 | } finally { 75 | setIsLoading(false); 76 | } 77 | }; 78 | 79 | const getFormData = async () => { 80 | const api = `/supplier/get-form`; 81 | setIsGetting(true); 82 | try { 83 | const res = await handleAPI(api); 84 | res.data && setFormData(res.data); 85 | } catch (error) { 86 | console.log(error); 87 | } finally { 88 | setIsGetting(false); 89 | } 90 | }; 91 | const handleClose = () => { 92 | form.resetFields(); 93 | setFile(undefined); 94 | onClose(); 95 | }; 96 | 97 | return ( 98 | form.submit()} 105 | okButtonProps={{ 106 | loading: isLoading, 107 | }} 108 | title={supplier ? 'Update' : 'Add Supplier'} 109 | okText={supplier ? 'Update' : `Add Supplier`} 110 | cancelText='Discard'> 111 | 135 | {formData && ( 136 |
144 | {formData.formItems.map((item) => ( 145 | 146 | ))} 147 | 148 | )} 149 | 150 |
151 | setFile(val.target.files[0])} 158 | /> 159 |
160 |
161 | ); 162 | }; 163 | 164 | export default ToogleSupplier; 165 | -------------------------------------------------------------------------------- /src/modals/index.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import AddPromotion from './AddPromotion'; 4 | import AddSubProductModal from './AddSubProductModal'; 5 | import ModalCategory from './ModalCategory'; 6 | import ModalExportData from './ModalExportData'; 7 | import ToogleSupplier from './ToogleSupplier'; 8 | 9 | export { 10 | ToogleSupplier, 11 | ModalExportData, 12 | ModalCategory, 13 | AddSubProductModal, 14 | AddPromotion, 15 | }; 16 | -------------------------------------------------------------------------------- /src/models/BillModel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export interface BillModel { 4 | _id: string; 5 | products: Product[]; 6 | total: number; 7 | status: number; 8 | customer_id: string; 9 | shippingAddress: ShippingAddress; 10 | paymentStatus: number; 11 | paymentMethod: string; 12 | createdAt: string; 13 | updatedAt: string; 14 | __v: number; 15 | } 16 | 17 | export interface Product { 18 | _id: string; 19 | createdBy: string; 20 | count: number; 21 | cost: number; 22 | subProductId: string; 23 | image: string; 24 | size: string; 25 | color: string; 26 | price: number; 27 | qty: number; 28 | productId: string; 29 | title: string; 30 | __v: number; 31 | } 32 | 33 | export interface ShippingAddress { 34 | address: string; 35 | _id: string; 36 | } 37 | 38 | export const BillStatus = ['pending', 'shipping', 'success', 'cancel']; 39 | export const BillStatusColor = ['warning', 'processing', 'success', 'error']; 40 | -------------------------------------------------------------------------------- /src/models/FormModel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { FormLayout } from 'antd/es/form/Form'; 4 | 5 | export interface FormModel { 6 | title: string; 7 | layout?: FormLayout; 8 | labelCol: number; 9 | wrapperCol: number; 10 | formItems: FormItemModel[]; 11 | } 12 | 13 | export interface FormItemModel { 14 | key: string; 15 | value: string; 16 | label: string; 17 | placeholder: string; 18 | type: 'default' | 'select' | 'number' | 'email' | 'tel' | 'file' | 'checkbox'; 19 | lockup_item: SelectModel[]; 20 | required: boolean; 21 | message: string; 22 | default_value: string; 23 | } 24 | 25 | export interface SelectModel { 26 | label: string; 27 | value: string; 28 | } 29 | 30 | export interface TreeModel { 31 | label: string; 32 | value: string; 33 | children?: SelectModel[]; 34 | } 35 | -------------------------------------------------------------------------------- /src/models/LogModel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export interface LogModel { 4 | _id: string; 5 | email: string; 6 | method: string; 7 | url: string; 8 | createdAt: Date; 9 | updatedAt: Date; 10 | } 11 | -------------------------------------------------------------------------------- /src/models/NotificationModel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export interface NotificationModel { 4 | _id: string; 5 | title: string; 6 | body: string; 7 | id: string; 8 | url: string; 9 | isRead: boolean; 10 | fromId: string; 11 | toId: string; 12 | to: string; 13 | createdAt: string; 14 | updatedAt: string; 15 | __v: number; 16 | } 17 | -------------------------------------------------------------------------------- /src/models/OrderModel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ProductModel, SubProductModel } from './Products'; 4 | 5 | export interface OrderModel { 6 | _id: string; 7 | user_id: string; 8 | product_id: string; 9 | total: number; 10 | quantity: number; 11 | subProduct_id: string; 12 | cost: number; 13 | status: string; 14 | price: number; 15 | createdAt: string; 16 | updatedAt: string; 17 | __v: number; 18 | product?: ProductModel; 19 | subProduct?: SubProductModel; 20 | } 21 | -------------------------------------------------------------------------------- /src/models/Products.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export interface CategoyModel { 4 | _id: string; 5 | title: string; 6 | parentId: string; 7 | slug: string; 8 | description: string; 9 | createdAt: string; 10 | updatedAt: string; 11 | __v: number; 12 | } 13 | 14 | export interface ProductModel { 15 | _id: string; 16 | title: string; 17 | slug: string; 18 | description: string; 19 | categories: string[]; 20 | supplier: string; 21 | createdAt: string; 22 | updatedAt: string; 23 | __v: number; 24 | isDeleted: boolean; 25 | subItems: SubProductModel[]; 26 | } 27 | 28 | export interface SubProductModel { 29 | size: string; 30 | color: string; 31 | price: number; 32 | qty: number; 33 | cost: number; 34 | productId: string; 35 | images: any[]; 36 | _id: string; 37 | createdAt: string; 38 | updatedAt: string; 39 | __v: number; 40 | } 41 | -------------------------------------------------------------------------------- /src/models/PromotionModel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export interface PromotionModel { 4 | _id: string; 5 | title: string; 6 | description: string; 7 | code: string; 8 | value: number; 9 | numOfAvailable: number; 10 | type: string; 11 | startAt: string; 12 | endAt: string; 13 | imageURL: string; 14 | createdAt: string; 15 | updatedAt: string; 16 | __v: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/models/SelectModel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export interface SelectModel { 4 | label: string; 5 | value: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/models/StatictisModel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ReactNode } from 'react'; 4 | 5 | export interface StatisticModel { 6 | key: string; 7 | description: string; 8 | value: number; 9 | valueType: 'number' | 'curency'; 10 | type?: 'horizontal' | 'vertical'; 11 | icon: ReactNode; 12 | color: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/models/SupplierModel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export interface SupplierModel { 4 | index: number; 5 | name: string; 6 | slug: string; 7 | product: string; 8 | categories: string[]; 9 | price: number; 10 | contact: string; 11 | isTaking: number; 12 | photoUrl: string; 13 | createdAt: string; 14 | updatedAt: string; 15 | email: string; 16 | active: string; 17 | _id: string; 18 | } 19 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/redux/reducers/authReducer.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { createSlice } from '@reduxjs/toolkit'; 4 | import { localDataNames } from '../../constants/appInfos'; 5 | 6 | export interface AuthState { 7 | token: string; 8 | _id: string; 9 | name: string; 10 | rule: number; 11 | } 12 | 13 | const initialState = { 14 | token: '', 15 | _id: '', 16 | name: '', 17 | rule: 0, 18 | }; 19 | 20 | const authSlice = createSlice({ 21 | name: 'auth', 22 | initialState: { 23 | data: initialState, 24 | }, 25 | reducers: { 26 | addAuth: (state, action) => { 27 | state.data = action.payload; 28 | syncLocal(action.payload); 29 | }, 30 | removeAuth: (state, _action) => { 31 | state.data = initialState; 32 | syncLocal({}); 33 | }, 34 | refreshtoken: (state, action) => { 35 | state.data.token = action.payload; 36 | }, 37 | }, 38 | }); 39 | 40 | export const authReducer = authSlice.reducer; 41 | export const { addAuth, removeAuth, refreshtoken } = authSlice.actions; 42 | 43 | export const authSeletor = (state: any) => state.authReducer.data; 44 | 45 | const syncLocal = (data: any) => { 46 | localStorage.setItem(localDataNames.authData, JSON.stringify(data)); 47 | }; 48 | -------------------------------------------------------------------------------- /src/redux/store.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { configureStore } from '@reduxjs/toolkit'; 4 | import { authReducer } from './reducers/authReducer'; 5 | 6 | const store = configureStore({ 7 | reducer: { 8 | authReducer, 9 | }, 10 | }); 11 | 12 | export default store; 13 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/routers/AuthRouter.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 4 | import { Login, SignUp } from '../screens'; 5 | import { Typography } from 'antd'; 6 | 7 | const { Title } = Typography; 8 | 9 | const AuthRouter = () => { 10 | return ( 11 |
12 |
13 |
16 |
17 | 25 |
26 |
27 | KANBAN 28 |
29 |
30 | 31 |
32 | 33 | 34 | } /> 35 | } /> 36 | 37 | 38 |
39 |
40 |
41 | ); 42 | }; 43 | 44 | export default AuthRouter; 45 | -------------------------------------------------------------------------------- /src/routers/MainRouter.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Affix, Layout } from 'antd'; 4 | import HomeScreen from '../screens/HomeScreen'; 5 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 6 | import { 7 | Actions, 8 | BillsScreen, 9 | Inventories, 10 | ManageStore, 11 | Orders, 12 | ProductDetail, 13 | ReportScreen, 14 | Suppliers, 15 | } from '../screens'; 16 | import { HeaderComponent, SiderComponent } from '../components'; 17 | import AddProduct from '../screens/inventories/AddProduct'; 18 | import Categories from '../screens/categories/Categories'; 19 | import CategoryDetail from '../screens/categories/CategoryDetail'; 20 | import PromotionScreen from '../screens/PromotionScreen'; 21 | import AddOrder from '../screens/orther/AddOrder'; 22 | import { 23 | Chart as ChartJS, 24 | CategoryScale, 25 | LinearScale, 26 | BarElement, 27 | Title, 28 | Tooltip, 29 | Legend, 30 | PointElement, 31 | LineElement, 32 | } from 'chart.js'; 33 | 34 | ChartJS.register( 35 | CategoryScale, 36 | LinearScale, 37 | LinearScale, 38 | PointElement, 39 | LineElement, 40 | BarElement, 41 | Title, 42 | Tooltip, 43 | Legend 44 | ); 45 | const { Content, Footer, Header, Sider } = Layout; 46 | 47 | const MainRouter = () => { 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 62 | 63 | } /> 64 | 65 | } /> 66 | } /> 67 | } 70 | /> 71 | 72 | } /> 73 | } /> 74 | 75 | } /> 76 | } /> 77 | 78 | 79 | } /> 80 | } 83 | /> 84 | 85 | 86 | } /> 87 | } /> 88 | } /> 89 | 90 | } index /> 91 | 92 | 93 | 94 |
95 | 96 | 97 | 98 | ); 99 | }; 100 | 101 | export default MainRouter; 102 | -------------------------------------------------------------------------------- /src/routers/Routers.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import { addAuth, authSeletor, AuthState } from '../redux/reducers/authReducer'; 5 | import AuthRouter from './AuthRouter'; 6 | import MainRouter from './MainRouter'; 7 | import { useEffect, useState } from 'react'; 8 | import { localDataNames } from '../constants/appInfos'; 9 | import { Spin } from 'antd'; 10 | 11 | const Routers = () => { 12 | const [isLoading, setIsLoading] = useState(false); 13 | 14 | const auth: AuthState = useSelector(authSeletor); 15 | const dispatch = useDispatch(); 16 | 17 | useEffect(() => { 18 | getData(); 19 | }, []); 20 | 21 | const getData = async () => { 22 | const res = localStorage.getItem(localDataNames.authData); 23 | res && dispatch(addAuth(JSON.parse(res))); 24 | }; 25 | 26 | return isLoading ? : !auth.token ? : ; 27 | }; 28 | 29 | export default Routers; 30 | -------------------------------------------------------------------------------- /src/screens/Actions.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import { LogModel } from '../models/LogModel'; 5 | import handleAPI from '../apis/handleAPI'; 6 | import { ColumnProps } from 'antd/es/table'; 7 | import { Table } from 'antd'; 8 | 9 | const Actions = () => { 10 | const [isLoading, setIsLoading] = useState(false); 11 | const [logs, setLogs] = useState([]); 12 | const [api, setApi] = useState(''); 13 | const [total, setTotal] = useState(0); 14 | const [limit, setLimit] = useState(20); 15 | const [page, setPage] = useState(1); 16 | 17 | useEffect(() => { 18 | setApi(`/logs?limit=${limit}&page=${page}`); 19 | }, []); 20 | 21 | useEffect(() => { 22 | setApi(`/logs?limit=${limit}&page=${page}`); 23 | }, [page, limit]); 24 | 25 | useEffect(() => { 26 | api && getLogs(api); 27 | }, [api]); 28 | 29 | const getLogs = async (url: string) => { 30 | setIsLoading(true); 31 | try { 32 | const res = await handleAPI(url); 33 | setLogs(res.data.items); 34 | setTotal(res.data.total); 35 | } catch (error) { 36 | console.log(error); 37 | } finally { 38 | setIsLoading(false); 39 | } 40 | }; 41 | 42 | const columns: ColumnProps[] = [ 43 | { 44 | key: '#', 45 | title: '#', 46 | dataIndex: 'id', 47 | render: (text, record, index) => index + 1, 48 | align: 'center', 49 | width: 50, 50 | }, 51 | { 52 | key: 'method', 53 | title: 'Method', 54 | dataIndex: 'method', 55 | }, 56 | { 57 | key: 'url', 58 | title: 'URL', 59 | dataIndex: 'url', 60 | }, 61 | 62 | { 63 | key: 'createdAt', 64 | title: 'Created At', 65 | dataIndex: 'createdAt', 66 | render: (text, record) => new Date(record.createdAt).toLocaleString(), 67 | align: 'center', 68 | width: 200, 69 | }, 70 | ]; 71 | 72 | return ( 73 |
74 |
{ 80 | setPage(page); 81 | setLimit(limit); 82 | }, 83 | }} 84 | columns={columns} 85 | dataSource={logs} 86 | loading={isLoading} 87 | size='small' 88 | /> 89 | 90 | ); 91 | }; 92 | 93 | export default Actions; 94 | -------------------------------------------------------------------------------- /src/screens/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Card, Spin, Typography } from 'antd'; 4 | import { useEffect, useState } from 'react'; 5 | import handleAPI from '../apis/handleAPI'; 6 | import { 7 | SalesAndPurchaseStatistic, 8 | StatisticComponent, 9 | TopSellingAndLowQuantityStatictis, 10 | } from '../components'; 11 | import { BillModel } from '../models/BillModel'; 12 | import { VND } from '../utils/handleCurrency'; 13 | 14 | const HomeScreen = () => { 15 | const [isLoading, setIsLoading] = useState(false); 16 | const [statictisValues, setStatictisValues] = useState<{ 17 | sales?: BillModel[]; 18 | products: number; 19 | suppliers: number; 20 | orders: number; 21 | totalOrder: number; 22 | subProduct: number; 23 | totalSubProduct: number; 24 | }>(); 25 | useEffect(() => { 26 | getStatistics(); 27 | }, []); 28 | 29 | const getStatistics = async () => { 30 | setIsLoading(true); 31 | const api = `/payments/statistics`; 32 | 33 | try { 34 | const res = await handleAPI(api); 35 | // console.log(res); 36 | setStatictisValues(res.data); 37 | } catch (error) { 38 | console.log(error); 39 | } finally { 40 | setIsLoading(false); 41 | } 42 | }; 43 | 44 | const totalcost = (value: BillModel[]) => { 45 | const items = value.map((item) => { 46 | return item.products.reduce((a, b) => a + b.price * (b.cost ?? 0), 0); 47 | }); 48 | return items.reduce((a, b) => a + b, 0); 49 | }; 50 | 51 | return isLoading ? ( 52 |
53 | 54 |
55 | ) : ( 56 |
57 |
58 |
59 | {statictisValues?.sales && statictisValues.sales.length > 0 && ( 60 | 61 | Sales Overviews 62 |
63 | 68 | a + b.total, 0) 71 | )} 72 | title='Revenue' 73 | color='#DE5AFF' 74 | image='./access/icons8-revenue-50.png' 75 | /> 76 | 77 | a + b.total, 0) - 80 | totalcost(statictisValues.sales) 81 | )} 82 | title='Profit' 83 | color='#FFB946' 84 | image='./access/icons8-profit-80.png' 85 | /> 86 | 87 | 93 |
94 |
95 | )} 96 |
97 |
98 |
99 |
100 | {statictisValues?.sales && statictisValues.sales.length > 0 && ( 101 | 102 |
103 |
104 | element.paymentStatus === 1) 108 | .reduce( 109 | (a, b) => 110 | a + 111 | (b.products.length > 0 ? b.products.length : 0), 112 | 0 113 | ) 114 | .toLocaleString()}`} 115 | title='Quantity in Hand' 116 | /> 117 |
118 |
119 | element.paymentStatus === 2) 123 | .reduce( 124 | (a, b) => 125 | a + 126 | (b.products.length > 0 ? b.products.length : 0), 127 | 0 128 | ) 129 | .toLocaleString()}`} 130 | title='To be received' 131 | /> 132 |
133 |
134 |
135 | )} 136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | {statictisValues?.orders && statictisValues.subProduct > 0 && ( 144 | 145 | Purchase Overview 146 |
147 | 152 | 158 | 159 | 165 | 166 | 172 |
173 |
174 | )} 175 |
176 |
177 |
178 |
179 | {statictisValues?.products && statictisValues.suppliers && ( 180 | 181 |
182 |
183 | 188 |
189 |
190 | 195 |
196 |
197 |
198 | )} 199 |
200 |
201 |
202 |
203 |
204 | 205 | 206 | 207 |
208 | ); 209 | }; 210 | 211 | export default HomeScreen; 212 | -------------------------------------------------------------------------------- /src/screens/ManageStore.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | const ManageStore = () => { 4 | return
ManageStore
; 5 | }; 6 | 7 | export default ManageStore; 8 | -------------------------------------------------------------------------------- /src/screens/PromotionScreen.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { ColumnProps } from 'antd/es/table'; 4 | import { useEffect, useState } from 'react'; 5 | import handleAPI from '../apis/handleAPI'; 6 | import { AddPromotion } from '../modals'; 7 | import { PromotionModel } from '../models/PromotionModel'; 8 | import { Avatar, Button, Image, Modal, Space, Table } from 'antd'; 9 | import { Edit2, Trash } from 'iconsax-react'; 10 | 11 | const { confirm } = Modal; 12 | 13 | const PromotionScreen = () => { 14 | const [isVisibleModalAddPromotion, setIsVisibleModalAddPromotion] = 15 | useState(false); 16 | const [isLoading, setIsLoading] = useState(false); 17 | const [promotions, setPromotions] = useState([]); 18 | const [promotionSelected, setPromotionSelected] = useState(); 19 | 20 | useEffect(() => { 21 | getPromotions(); 22 | }, []); 23 | 24 | const getPromotions = async () => { 25 | const api = `/promotions/`; 26 | setIsLoading(true); 27 | try { 28 | const res = await handleAPI(api); 29 | setPromotions(res.data); 30 | } catch (error) { 31 | console.log(error); 32 | } finally { 33 | setIsLoading(false); 34 | } 35 | }; 36 | 37 | const handleRemovePromotion = async (id: string) => { 38 | const api = `/promotions/remove?id=${id}`; 39 | 40 | try { 41 | await handleAPI(api, undefined, 'delete'); 42 | await getPromotions(); 43 | } catch (error) { 44 | console.log(error); 45 | } 46 | }; 47 | 48 | const columns: ColumnProps[] = [ 49 | { 50 | key: 'image', 51 | dataIndex: 'imageURL', 52 | title: 'Image', 53 | render: (img: string) => , 54 | }, 55 | { 56 | key: 'title', 57 | dataIndex: 'title', 58 | title: 'Title', 59 | }, 60 | { 61 | key: 'description', 62 | dataIndex: 'description', 63 | title: 'Description', 64 | }, 65 | { 66 | key: 'code', 67 | dataIndex: 'code', 68 | title: 'Code', 69 | }, 70 | { 71 | key: 'available', 72 | dataIndex: 'numOfAvailable', 73 | title: 'Available', 74 | }, 75 | 76 | { 77 | key: 'value', 78 | dataIndex: 'value', 79 | title: 'Value', 80 | }, 81 | { 82 | key: 'type', 83 | dataIndex: 'type', 84 | title: 'Type', 85 | }, 86 | { 87 | key: 'btn', 88 | dataIndex: '', 89 | align: 'right', 90 | fixed: 'right', 91 | render: (item: PromotionModel) => ( 92 | 93 |
121 | 122 | 123 | await getPromotions()} 126 | visible={isVisibleModalAddPromotion} 127 | onClose={() => setIsVisibleModalAddPromotion(false)} 128 | /> 129 | 130 | ); 131 | }; 132 | 133 | export default PromotionScreen; 134 | -------------------------------------------------------------------------------- /src/screens/ReportScreen.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Card, Divider, Empty, Statistic, Table } from 'antd'; 4 | import React, { useEffect, useState } from 'react'; 5 | import { Link } from 'react-router-dom'; 6 | import { BillModel } from '../models/BillModel'; 7 | import { OrderModel } from '../models/OrderModel'; 8 | import handleAPI from '../apis/handleAPI'; 9 | import { VND } from '../utils/handleCurrency'; 10 | import { CategoyModel } from '../models/Products'; 11 | 12 | interface TopCategoryModel extends CategoyModel { 13 | count: number; 14 | total: number; 15 | } 16 | 17 | const ReportScreen = () => { 18 | const [totalProfitDatas, setTotalProfitDatas] = useState<{ 19 | bills: BillModel[]; 20 | orders: OrderModel[]; 21 | revenue: number; 22 | profitMonth: number; 23 | profitYear: number; 24 | }>(); 25 | const [loadings, setLoadings] = useState({ 26 | loadingTotalProfitDatas: false, 27 | loadingTopCategories: false, 28 | }); 29 | const [topCategories, setTopCategories] = useState([]); 30 | 31 | useEffect(() => { 32 | getDatas(); 33 | }, []); 34 | 35 | const getDatas = async () => { 36 | try { 37 | await getTotalProfitDatas(); 38 | await getTopCategories(); 39 | } catch (error) { 40 | console.log(error); 41 | setLoadings({ 42 | loadingTotalProfitDatas: false, 43 | loadingTopCategories: false, 44 | }); 45 | } 46 | }; 47 | 48 | const getTotalProfitDatas = async () => { 49 | setLoadings({ ...loadings, loadingTotalProfitDatas: true }); 50 | 51 | const res = await handleAPI(`/admin/total-profit`); 52 | setTotalProfitDatas(res.data); 53 | 54 | setLoadings({ ...loadings, loadingTotalProfitDatas: false }); 55 | }; 56 | 57 | const getTopCategories = async () => { 58 | setLoadings({ ...loadings, loadingTopCategories: true }); 59 | const res = await handleAPI(`/admin/top-categories`); 60 | setTopCategories(res.data); 61 | setLoadings({ ...loadings, loadingTopCategories: false }); 62 | }; 63 | 64 | return ( 65 |
66 |
67 |
68 | 69 | {totalProfitDatas ? ( 70 | <> 71 |
72 |
73 | a + b.total, 0) 77 | )} 78 | /> 79 |
80 |
81 | 85 |
86 |
87 | a + b.total, 0) 91 | )} 92 | /> 93 |
94 |
95 | 96 |
97 |
98 | 102 |
103 |
104 | 108 |
109 |
110 | 118 |
119 |
120 | 124 |
125 |
126 | 127 | ) : ( 128 | 129 | )} 130 |
131 |
132 |
133 | See all}> 137 |
VND.format(total), 160 | }, 161 | ]} 162 | /> 163 | 164 | 165 | 166 | 167 | ); 168 | }; 169 | 170 | export default ReportScreen; 171 | -------------------------------------------------------------------------------- /src/screens/Suppliers.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Button, Empty, message, Modal, Space, Table, Typography } from 'antd'; 4 | import { ColumnProps } from 'antd/es/table'; 5 | import { Edit2, Sort, UserRemove } from 'iconsax-react'; 6 | import { useEffect, useState } from 'react'; 7 | import handleAPI from '../apis/handleAPI'; 8 | import { colors } from '../constants/colors'; 9 | import { ToogleSupplier } from '../modals'; 10 | import { SupplierModel } from '../models/SupplierModel'; 11 | import { FormModel } from '../models/FormModel'; 12 | import TableComponet from '../components/TableComponent'; 13 | import { useNavigate, useRoutes, useSearchParams } from 'react-router-dom'; 14 | 15 | const { Title, Text } = Typography; 16 | const { confirm } = Modal; 17 | 18 | const Suppliers = () => { 19 | const [isVisibleModalAddNew, setIsVisibleModalAddNew] = useState(false); 20 | const [suppliers, setSuppliers] = useState([]); 21 | const [isLoading, setIsLoading] = useState(false); 22 | const [supplierSelected, setSupplierSelected] = useState(); 23 | const [page, setPage] = useState(1); 24 | const [pageSize, setPageSize] = useState(10); 25 | const [total, setTotal] = useState(10); 26 | const [forms, setForms] = useState(); 27 | 28 | useEffect(() => { 29 | getData(); 30 | }, []); 31 | 32 | useEffect(() => { 33 | getSuppliers(); 34 | }, [page, pageSize]); 35 | 36 | const getData = async () => { 37 | setIsLoading(true); 38 | try { 39 | await getFroms(); 40 | } catch (error: any) { 41 | message.error(error.message); 42 | } finally { 43 | setIsLoading(false); 44 | } 45 | }; 46 | 47 | const getFroms = async () => { 48 | const api = `/supplier/get-form`; 49 | const res = await handleAPI(api); 50 | 51 | res.data && setForms(res.data); 52 | }; 53 | 54 | const getSuppliers = async () => { 55 | const api = `/supplier?page=${page}&pageSize=${pageSize}`; 56 | setIsLoading(true); 57 | try { 58 | const res = await handleAPI(api); 59 | res.data && setSuppliers(res.data.items); 60 | console.log(res.data); 61 | const items: SupplierModel[] = []; 62 | 63 | res.data.items.forEach((item: any, index: number) => 64 | items.push({ 65 | index: (page - 1) * pageSize + (index + 1), 66 | ...item, 67 | }) 68 | ); 69 | 70 | setSuppliers(items); 71 | setTotal(res.data.total); 72 | } catch (error: any) { 73 | message.error(error.message); 74 | } finally { 75 | setIsLoading(false); 76 | } 77 | }; 78 | 79 | const removeSuppiler = async (id: string) => { 80 | try { 81 | await handleAPI(`/supplier/remove?id=${id}`, undefined, 'delete'); 82 | 83 | await getSuppliers(); 84 | message.error('Remove supplier successfully!!!'); 85 | } catch (error) { 86 | console.log(error); 87 | } 88 | }; 89 | 90 | return forms ? ( 91 |
92 | { 95 | setPage(val.page); 96 | setPageSize(val.pageSize); 97 | }} 98 | onAddNew={() => { 99 | setIsVisibleModalAddNew(true); 100 | }} 101 | loading={isLoading} 102 | forms={forms} 103 | records={suppliers} 104 | total={total} 105 | extraColumn={(item) => ( 106 | 107 |
142 | ) : ( 143 | 144 | ); 145 | }; 146 | 147 | export default Suppliers; 148 | -------------------------------------------------------------------------------- /src/screens/auth/Login.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { 4 | Button, 5 | Card, 6 | Checkbox, 7 | Form, 8 | Input, 9 | message, 10 | Space, 11 | Typography, 12 | } from 'antd'; 13 | import { useState } from 'react'; 14 | import { useDispatch } from 'react-redux'; 15 | import { Link } from 'react-router-dom'; 16 | import SocialLogin from './components/SocialLogin'; 17 | import handleAPI from '../../apis/handleAPI'; 18 | import { addAuth } from '../../redux/reducers/authReducer'; 19 | import { appInfo, localDataNames } from '../../constants/appInfos'; 20 | 21 | const { Title, Paragraph, Text } = Typography; 22 | 23 | const Login = () => { 24 | const [isLoading, setIsLoading] = useState(false); 25 | const [isRemember, setIsRemember] = useState(false); 26 | 27 | const [form] = Form.useForm(); 28 | const dispatch = useDispatch(); 29 | 30 | const handleLogin = async (values: { email: string; password: string }) => { 31 | setIsLoading(true); 32 | try { 33 | const res: any = await handleAPI('/auth/login', values, 'post'); 34 | 35 | message.success(res.message); 36 | res.data && dispatch(addAuth(res.data)); 37 | 38 | if (isRemember) { 39 | localStorage.setItem(localDataNames.authData, JSON.stringify(res.data)); 40 | } 41 | } catch (error: any) { 42 | message.error(error.message); 43 | } finally { 44 | setIsLoading(false); 45 | } 46 | }; 47 | 48 | return ( 49 | <> 50 | 54 |
55 | 64 | Log in to your account 65 | 66 | Welcome back! please enter your details 67 | 68 |
69 | 70 |
76 | 85 | 86 | 87 | 96 | 97 | 98 | 99 | 100 |
101 |
102 | setIsRemember(val.target.checked)}> 105 | Remember for 30 days 106 | 107 |
108 |
109 | Forgot password? 110 |
111 |
112 | 113 |
114 | 124 |
125 | 126 |
127 | 128 | Don't have an acount? 129 | Sign up 130 | 131 |
132 |
133 | 134 | ); 135 | }; 136 | 137 | export default Login; 138 | -------------------------------------------------------------------------------- /src/screens/auth/SignUp.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Button, Card, Form, Input, message, Space, Typography } from 'antd'; 4 | import { useState } from 'react'; 5 | import { useDispatch } from 'react-redux'; 6 | import { Link } from 'react-router-dom'; 7 | import handleAPI from '../../apis/handleAPI'; 8 | import { addAuth } from '../../redux/reducers/authReducer'; 9 | import SocialLogin from './components/SocialLogin'; 10 | 11 | const { Title, Text, Paragraph } = Typography; 12 | const SignUp = () => { 13 | const [isLoading, setIsLoading] = useState(false); 14 | 15 | const dispatch = useDispatch(); 16 | 17 | const [form] = Form.useForm(); 18 | 19 | const handleLogin = async (values: { email: string; password: string }) => { 20 | const api = `/auth/register`; 21 | 22 | setIsLoading(true); 23 | try { 24 | const res: any = await handleAPI(api, values, 'post'); 25 | if (res.data) { 26 | message.success(res.message); 27 | dispatch(addAuth(res.data)); 28 | } 29 | } catch (error: any) { 30 | console.log(error); 31 | message.error(error.message); 32 | } finally { 33 | setIsLoading(false); 34 | } 35 | }; 36 | 37 | return ( 38 | <> 39 | 43 |
44 | Create an account 45 | Free trial 30 days 46 |
47 | 48 |
54 | 63 | 64 | 65 | 74 | 80 | 81 | ({ 90 | validator: (_, value) => { 91 | if (!value) { 92 | return Promise.reject( 93 | new Error('Please enter your password!!!') 94 | ); 95 | } else if (value.length < 6) { 96 | return Promise.reject( 97 | new Error('Please enter your password!!!') 98 | ); 99 | } else { 100 | return Promise.resolve(); 101 | } 102 | }, 103 | }), 104 | ]}> 105 | 110 | 111 | 112 | 113 |
114 | 124 |
125 | 126 |
127 | 128 | Already an acount? 129 | Login 130 | 131 |
132 |
133 | 134 | ); 135 | }; 136 | 137 | export default SignUp; 138 | -------------------------------------------------------------------------------- /src/screens/auth/components/SocialLogin.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Button, message } from 'antd'; 4 | import { GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; 5 | import { useState } from 'react'; 6 | import { useDispatch } from 'react-redux'; 7 | import handleAPI from '../../../apis/handleAPI'; 8 | import { auth } from '../../../firebase/firebaseConfig'; 9 | import { addAuth } from '../../../redux/reducers/authReducer'; 10 | import { localDataNames } from '../../../constants/appInfos'; 11 | 12 | const provider = new GoogleAuthProvider(); 13 | provider.addScope('https://www.googleapis.com/auth/contacts.readonly'); 14 | // provider.setCustomParameters({ 15 | // login_hint: 'bsdaoquang@gmail.com', 16 | // }); 17 | 18 | interface Props { 19 | isRemember?: boolean; 20 | } 21 | 22 | const SocialLogin = (props: Props) => { 23 | const { isRemember } = props; 24 | 25 | const [isLoading, setIsLoading] = useState(false); 26 | 27 | const dispatch = useDispatch(); 28 | 29 | const handleLoginWithGoogle = async () => { 30 | setIsLoading(true); 31 | try { 32 | const result = await signInWithPopup(auth, provider); 33 | if (result) { 34 | const user = result.user; 35 | 36 | if (user) { 37 | const data = { 38 | name: user.displayName, 39 | email: user.email, 40 | photoUrl: user.photoURL, 41 | }; 42 | 43 | const api = `/auth/google-login`; 44 | 45 | try { 46 | const res: any = await handleAPI(api, data, 'post'); 47 | message.success(res.message); 48 | dispatch(addAuth(res.data)); 49 | 50 | if (isRemember) { 51 | localStorage.setItem( 52 | localDataNames.authData, 53 | JSON.stringify(res.data) 54 | ); 55 | } 56 | } catch (error: any) { 57 | console.log(error); 58 | message.error(error.message); 59 | } finally { 60 | setIsLoading(false); 61 | } 62 | } 63 | } else { 64 | console.log('Can not login with google'); 65 | } 66 | } catch (error) { 67 | console.log(error); 68 | } finally { 69 | setIsLoading(false); 70 | } 71 | }; 72 | 73 | return ( 74 | 91 | ); 92 | }; 93 | 94 | export default SocialLogin; 95 | -------------------------------------------------------------------------------- /src/screens/bills/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useEffect, useState } from 'react'; 4 | import { BillModel, BillStatus, BillStatusColor } from '../../models/BillModel'; 5 | import handleAPI from '../../apis/handleAPI'; 6 | import { ColumnProps } from 'antd/es/table'; 7 | import { DatePicker, Input, Table, Tag, Typography } from 'antd'; 8 | 9 | const BillsScreen = () => { 10 | const [isLoading, setIsLoading] = useState(false); 11 | const [bills, setBills] = useState([]); 12 | const [total, setTotal] = useState(0); 13 | const [limit, setLimit] = useState(20); 14 | const [page, setPage] = useState(1); 15 | const [api, setApi] = useState(''); 16 | 17 | useEffect(() => { 18 | setApi(`/payments/bills?limit=${limit}&page=${page}`); 19 | }, []); 20 | 21 | useEffect(() => { 22 | setApi(`/payments/bills?limit=${limit}&page=${page}`); 23 | }, [limit, page]); 24 | 25 | useEffect(() => { 26 | api && getBills(api); 27 | }, [api]); 28 | 29 | const getBills = async (url: string) => { 30 | setIsLoading(true); 31 | try { 32 | const res = await handleAPI(url); 33 | setBills(res.data.items); 34 | setTotal(res.data.total); 35 | } catch (error) { 36 | console.log(error); 37 | } finally { 38 | setIsLoading(false); 39 | } 40 | }; 41 | 42 | const columns: ColumnProps[] = [ 43 | { 44 | title: '#', 45 | dataIndex: '_id', 46 | key: '#', 47 | render: (_id: string, record: BillModel, index: number) => index + 1, 48 | align: 'center', 49 | width: 50, 50 | }, 51 | { 52 | title: 'Customer', 53 | dataIndex: 'customer_id', 54 | key: 'customer', 55 | render: (customer: any) => customer, 56 | }, 57 | { 58 | title: 'Shipping Address', 59 | dataIndex: 'shippingAddress', 60 | key: 'shippingAddress', 61 | render: (shippingAddress: any) => '', 62 | }, 63 | { 64 | title: 'Payment Method', 65 | dataIndex: 'paymentMethod', 66 | key: 'paymentMethod', 67 | render: (paymentMethod: any) => ( 68 | 72 | {paymentMethod} 73 | 74 | ), 75 | align: 'center', 76 | }, 77 | { 78 | key: 'products', 79 | title: 'Products', 80 | dataIndex: 'products', 81 | render: (products: any) => products.length, 82 | align: 'right', 83 | }, 84 | { 85 | title: 'Amount', 86 | dataIndex: 'total', 87 | key: 'total', 88 | render: (total: number) => total.toLocaleString(), 89 | align: 'right', 90 | sorter: (a: BillModel, b: BillModel) => a.total - b.total, 91 | }, 92 | { 93 | title: 'Status', 94 | dataIndex: 'status', 95 | key: 'status', 96 | render: (status: number) => ( 97 | {BillStatus[status]} 98 | ), 99 | align: 'center', 100 | sorter: (a: BillModel, b: BillModel) => a.status - b.status, 101 | }, 102 | ]; 103 | 104 | return ( 105 |
106 |
107 |
108 |
109 | 110 |
111 |
112 | 113 |
114 |
115 |
116 |
129 | `${range[0]}-${range[1]} of ${total} items`, 130 | pageSize: limit, 131 | current: page, 132 | onChange: (page, limit) => { 133 | setPage(page); 134 | setLimit(limit); 135 | }, 136 | }} 137 | /> 138 | 139 | ); 140 | }; 141 | 142 | export default BillsScreen; 143 | -------------------------------------------------------------------------------- /src/screens/categories/Categories.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { 4 | Button, 5 | Card, 6 | message, 7 | Modal, 8 | Space, 9 | Spin, 10 | Tooltip, 11 | Table, 12 | } from 'antd'; 13 | import { ColumnProps } from 'antd/es/table'; 14 | import { Edit2, Trash } from 'iconsax-react'; 15 | import { useEffect, useState } from 'react'; 16 | import { Link } from 'react-router-dom'; 17 | import handleAPI from '../../apis/handleAPI'; 18 | import { colors } from '../../constants/colors'; 19 | import { TreeModel } from '../../models/FormModel'; 20 | import { CategoyModel } from '../../models/Products'; 21 | import { getTreeValues } from '../../utils/getTreeValues'; 22 | import { AddCategory } from '../../components'; 23 | 24 | const { confirm } = Modal; 25 | 26 | const Categories = () => { 27 | const [categories, setCategories] = useState([]); 28 | const [isLoading, setIsLoading] = useState(false); 29 | const [page, setPage] = useState(1); 30 | const [pageSize, setPageSize] = useState(10); 31 | const [treeValues, setTreeValues] = useState([]); 32 | const [categorySelected, setCategorySelected] = useState(); 33 | 34 | useEffect(() => { 35 | getCategories(`/products/get-categories`, true); 36 | }, []); 37 | 38 | useEffect(() => { 39 | const api = `/products/get-categories?page=${page}&pageSize=${pageSize}`; 40 | getCategories(api); 41 | }, [page, pageSize]); 42 | 43 | const getCategories = async (api: string, isSelect?: boolean) => { 44 | try { 45 | const res = await handleAPI(api); 46 | 47 | setCategories(getTreeValues(res.data, false)); 48 | 49 | if (isSelect) { 50 | setTreeValues(getTreeValues(res.data, true)); 51 | } 52 | } catch (error) { 53 | console.log(error); 54 | } finally { 55 | setIsLoading(false); 56 | } 57 | }; 58 | 59 | const columns: ColumnProps[] = [ 60 | { 61 | key: 'title', 62 | title: 'Name', 63 | dataIndex: '', 64 | render: (item: CategoyModel) => ( 65 | 66 | {item.title} 67 | 68 | ), 69 | }, 70 | { 71 | key: 'description', 72 | title: 'Description', 73 | dataIndex: 'description', 74 | }, 75 | { 76 | key: 'btnContainer', 77 | title: 'Actions', 78 | dataIndex: '', 79 | render: (item: any) => ( 80 | 81 | 82 |
160 | 161 | 162 | 163 | 164 | 165 | ); 166 | }; 167 | 168 | export default Categories; 169 | -------------------------------------------------------------------------------- /src/screens/categories/CategoryDetail.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { useParams, useSearchParams } from 'react-router-dom'; 4 | 5 | const CategoryDetail = () => { 6 | const [searchparams] = useSearchParams(); 7 | const params = useParams(); 8 | 9 | const id = searchparams.get('id'); 10 | 11 | return
CategoryDetail
; 12 | }; 13 | 14 | export default CategoryDetail; 15 | -------------------------------------------------------------------------------- /src/screens/index.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import Actions from './Actions'; 4 | import Login from './auth/Login'; 5 | import SignUp from './auth/SignUp'; 6 | import BillsScreen from './bills'; 7 | import Inventories from './inventories/Inventories'; 8 | import ProductDetail from './inventories/ProductDetail'; 9 | import ManageStore from './ManageStore'; 10 | import Orders from './orther'; 11 | import ReportScreen from './ReportScreen'; 12 | import Suppliers from './Suppliers'; 13 | 14 | export { 15 | Login, 16 | SignUp, 17 | ManageStore, 18 | Inventories, 19 | ReportScreen, 20 | Orders, 21 | Suppliers, 22 | ProductDetail, 23 | Actions, 24 | BillsScreen, 25 | }; 26 | -------------------------------------------------------------------------------- /src/screens/inventories/AddProduct.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Editor } from '@tinymce/tinymce-react'; 4 | import { 5 | Button, 6 | Card, 7 | Divider, 8 | Form, 9 | Input, 10 | message, 11 | Select, 12 | Space, 13 | Spin, 14 | TreeSelect, 15 | Typography, 16 | Image, 17 | Upload, 18 | UploadProps, 19 | } from 'antd'; 20 | import { useEffect, useRef, useState } from 'react'; 21 | import handleAPI from '../../apis/handleAPI'; 22 | import { SelectModel, TreeModel } from '../../models/FormModel'; 23 | import { replaceName } from '../../utils/replaceName'; 24 | import { uploadFile } from '../../utils/uploadFile'; 25 | import { Add } from 'iconsax-react'; 26 | import { ModalCategory } from '../../modals'; 27 | import { getTreeValues } from '../../utils/getTreeValues'; 28 | import { useSearchParams } from 'react-router-dom'; 29 | import ProductDetail from './ProductDetail'; 30 | 31 | const { Text, Title, Paragraph } = Typography; 32 | 33 | const AddProduct = () => { 34 | const [isLoading, setIsLoading] = useState(false); 35 | const [content, setcontent] = useState(''); 36 | const [supplierOptions, setSupplierOptions] = useState([]); 37 | const [isVisibleAddCategory, setIsVisibleAddCategory] = useState(false); 38 | const [categories, setCategories] = useState([]); 39 | const [isCreating, setIsCreating] = useState(false); 40 | const [fileUrl, setFileUrl] = useState(''); 41 | const [fileList, setFileList] = useState([]); 42 | 43 | const [searchParams] = useSearchParams(); 44 | 45 | const id = searchParams.get('id'); 46 | 47 | const editorRef = useRef(null); 48 | const [form] = Form.useForm(); 49 | 50 | useEffect(() => { 51 | getData(); 52 | }, []); 53 | 54 | useEffect(() => { 55 | if (id) { 56 | getProductDetail(id); 57 | } 58 | }, [id]); 59 | 60 | const getData = async () => { 61 | setIsLoading(true); 62 | try { 63 | await getSuppliers(); 64 | await getCategories(); 65 | } catch (error: any) { 66 | message.error(error.message); 67 | } finally { 68 | setIsLoading(false); 69 | } 70 | }; 71 | 72 | const getProductDetail = async (id: string) => { 73 | const api = `/products/detail?id=${id}`; 74 | try { 75 | const res = await handleAPI(api); 76 | const item = res.data; 77 | 78 | if (item) { 79 | form.setFieldsValue(item); 80 | setcontent(item.content); 81 | if (item.images && item.images.length > 0) { 82 | const items = [...fileList]; 83 | item.images.forEach((url: string) => 84 | items.push({ 85 | uid: `${Math.floor(Math.random() * 1000000)}`, 86 | name: url, 87 | status: 'done', 88 | url, 89 | }) 90 | ); 91 | 92 | setFileList(items); 93 | } 94 | } 95 | } catch (error) { 96 | console.log(error); 97 | } 98 | }; 99 | 100 | const handleAddNewProduct = async (values: any) => { 101 | const content = editorRef.current.getContent(); 102 | const data: any = {}; 103 | setIsCreating(true); 104 | for (const i in values) { 105 | data[`${i}`] = values[i] ?? ''; 106 | } 107 | 108 | data.content = content; 109 | data.slug = replaceName(values.title); 110 | 111 | if (fileList.length > 0) { 112 | const urls: string[] = []; 113 | fileList.forEach(async (file) => { 114 | if (file.originFileObj) { 115 | const url = await uploadFile(file.originFileObj); 116 | url && urls.push(url); 117 | } else { 118 | urls.push(file.url); 119 | } 120 | }); 121 | 122 | data.images = urls; 123 | } 124 | 125 | try { 126 | await handleAPI( 127 | `/products/${id ? `update?id=${id}` : 'add-new'}`, 128 | data, 129 | id ? 'put' : 'post' 130 | ); 131 | window.history.back(); 132 | } catch (error) { 133 | console.log(error); 134 | } finally { 135 | setIsCreating(false); 136 | } 137 | }; 138 | 139 | const getSuppliers = async () => { 140 | const api = `/supplier`; 141 | const res = await handleAPI(api); 142 | 143 | const data = res.data.items; 144 | const options = data.map((item: any) => ({ 145 | value: item._id, 146 | label: item.name, 147 | })); 148 | 149 | setSupplierOptions(options); 150 | }; 151 | 152 | const getCategories = async () => { 153 | const res = await handleAPI(`/products/get-categories`); 154 | const datas = res.data; 155 | 156 | const data = datas.length > 0 ? getTreeValues(datas, true) : []; 157 | 158 | setCategories(data); 159 | }; 160 | 161 | const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => { 162 | const items = newFileList.map((item) => 163 | item.originFileObj 164 | ? { 165 | ...item, 166 | url: item.originFileObj 167 | ? URL.createObjectURL(item.originFileObj) 168 | : '', 169 | status: 'done', 170 | } 171 | : { ...item } 172 | ); 173 | 174 | setFileList(items); 175 | }; 176 | 177 | return isLoading ? ( 178 | 179 | ) : ( 180 |
181 |
182 | Add new Product 183 |
189 |
190 |
191 | 200 | 201 | 202 | 203 | 209 | 210 | (editorRef.current = editor)} 214 | initialValue={content !== '' ? content : ''} 215 | init={{ 216 | height: 500, 217 | menubar: true, 218 | plugins: [ 219 | 'advlist', 220 | 'autolink', 221 | 'lists', 222 | 'link', 223 | 'image', 224 | 'charmap', 225 | 'preview', 226 | 'anchor', 227 | 'searchreplace', 228 | 'visualblocks', 229 | 'code', 230 | 'fullscreen', 231 | 'insertdatetime', 232 | 'media', 233 | 'table', 234 | 'code', 235 | 'help', 236 | 'wordcount', 237 | ], 238 | toolbar: 239 | 'undo redo | blocks | ' + 240 | 'bold italic forecolor | alignleft aligncenter ' + 241 | 'alignright alignjustify | bullist numlist outdent indent | ' + 242 | 'removeformat | help', 243 | content_style: 244 | 'body { font-family:Helvetica,Arial,sans-serif; font-size:14px }', 245 | }} 246 | /> 247 |
248 |
249 | 250 | 251 | 257 | 264 | 265 | 266 | 267 | 268 | ( 272 | <> 273 | {menu} 274 | 275 | 276 | 285 | 286 | )} 287 | /> 288 | 289 | 290 | 291 | 299 | setFileUrl(val.target.value)} 325 | className='mb-3' 326 | /> 327 | { 331 | const file = files.target.files[0]; 332 | 333 | if (file) { 334 | const donwloadUrl = await uploadFile(file); 335 | donwloadUrl && setFileUrl(donwloadUrl); 336 | } 337 | }} 338 | /> 339 | 340 |
341 |
342 | 343 |
344 | 345 | setIsVisibleAddCategory(false)} 348 | onAddNew={async (val) => { 349 | await getCategories(); 350 | }} 351 | values={categories} 352 | /> 353 |
354 | ); 355 | }; 356 | 357 | export default AddProduct; 358 | -------------------------------------------------------------------------------- /src/screens/inventories/Inventories.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { 4 | Avatar, 5 | Button, 6 | Divider, 7 | Dropdown, 8 | Input, 9 | message, 10 | Modal, 11 | Space, 12 | Table, 13 | Tag, 14 | Tooltip, 15 | Typography, 16 | } from 'antd'; 17 | import { ColumnProps, TableProps } from 'antd/es/table'; 18 | import { Edit2, Sort, Trash } from 'iconsax-react'; 19 | import React, { useEffect, useState } from 'react'; 20 | import { MdLibraryAdd } from 'react-icons/md'; 21 | import { Link, useNavigate } from 'react-router-dom'; 22 | import handleAPI from '../../apis/handleAPI'; 23 | import { FilterProduct } from '../../components'; 24 | import { FilterProductValue } from '../../components/FilterProduct'; 25 | import { colors, listColors } from '../../constants/colors'; 26 | import { AddSubProductModal } from '../../modals'; 27 | import { 28 | CategoyModel, 29 | ProductModel, 30 | SubProductModel, 31 | } from '../../models/Products'; 32 | import { replaceName } from '../../utils/replaceName'; 33 | 34 | const { confirm } = Modal; 35 | 36 | type TableRowSelection = 37 | TableProps['rowSelection']; 38 | 39 | const Inventories = () => { 40 | const [isLoading, setIsLoading] = useState(false); 41 | const [products, setProducts] = useState([]); 42 | const [isVisibleAddSubProduct, setIsVisibleAddSubProduct] = useState(false); 43 | const [productSelected, setProductSelected] = useState(); 44 | const [selectedRowKeys, setSelectedRowKeys] = useState([]); 45 | const [page, setPage] = useState(1); 46 | const [pageSize, setPageSize] = useState(10); 47 | const [total, setTotal] = useState(10); 48 | const [searchKey, setSearchKey] = useState(''); 49 | const [isFilting, setIsFilting] = useState(false); 50 | 51 | const navigate = useNavigate(); 52 | 53 | useEffect(() => { 54 | if (!searchKey) { 55 | setPage(1); 56 | getProducts(`/products?page=${page}&pageSize=${pageSize}`); 57 | } 58 | }, [searchKey]); 59 | 60 | const getProducts = async (api: string) => { 61 | setIsLoading(true); 62 | try { 63 | const res = await handleAPI(api); 64 | const data = res.data; 65 | setProducts(data.items.map((item: any) => ({ ...item, key: item._id }))); 66 | setTotal(data.totalItems); 67 | } catch (error) { 68 | console.log(error); 69 | } finally { 70 | setIsLoading(false); 71 | } 72 | }; 73 | 74 | const getMinMaxValues = (data: SubProductModel[]) => { 75 | const nums: number[] = []; 76 | 77 | if (data.length > 0) { 78 | data.forEach((item) => nums.push(item.price)); 79 | } 80 | 81 | return nums.length > 0 82 | ? `${Math.min(...nums).toLocaleString()} - ${Math.max( 83 | ...nums 84 | ).toLocaleString()}` 85 | : ''; 86 | }; 87 | 88 | const hanleRemoveProduct = async (id: string) => { 89 | const api = `/products/delete?id=${id}`; 90 | try { 91 | await handleAPI(api, undefined, 'delete'); 92 | const items = [...products]; 93 | const index = items.findIndex((element) => element._id === id); 94 | 95 | if (index !== -1) { 96 | items.splice(index, 1); 97 | } 98 | 99 | setProducts(items); 100 | 101 | message.success('Product removed!!!'); 102 | } catch (error: any) { 103 | message.error(error.message); 104 | } 105 | }; 106 | 107 | const onSelectChange = (newSelectRowKeys: React.Key[]) => { 108 | setSelectedRowKeys(newSelectRowKeys); 109 | }; 110 | 111 | const rowSelection: TableRowSelection = { 112 | selectedRowKeys, 113 | onChange: onSelectChange, 114 | }; 115 | 116 | const columns: ColumnProps[] = [ 117 | { 118 | key: 'title', 119 | dataIndex: '', 120 | title: 'Title', 121 | width: 300, 122 | render: (item: ProductModel) => ( 123 | 124 | {item.title} 125 | 126 | ), 127 | }, 128 | { 129 | key: 'description', 130 | dataIndex: 'description', 131 | title: 'description', 132 | width: 400, 133 | render: (desc: string) => ( 134 | 135 |
{desc}
136 |
137 | ), 138 | }, 139 | { 140 | key: 'categories', 141 | dataIndex: 'categories', 142 | title: 'categories', 143 | render: (cats: CategoyModel[]) => ( 144 | 145 | {cats.map((cat) => ( 146 | 147 | 152 | {cat.title} 153 | 154 | 155 | ))} 156 | 157 | ), 158 | width: 300, 159 | }, 160 | { 161 | key: 'images', 162 | dataIndex: 'images', 163 | title: 'Images', 164 | render: (imgs: string[]) => 165 | imgs && 166 | imgs.length > 0 && ( 167 | 168 | 169 | {imgs.map((img) => ( 170 | 171 | ))} 172 | 173 | 174 | ), 175 | width: 300, 176 | }, 177 | { 178 | key: 'colors', 179 | dataIndex: 'subItems', 180 | title: 'Color', 181 | render: (items: SubProductModel[]) => { 182 | const colors: string[] = []; 183 | 184 | items.forEach( 185 | (sub) => !colors.includes(sub.color) && colors.push(sub.color) 186 | ); 187 | 188 | return ( 189 | 190 | {colors.length > 0 && 191 | colors.map((item, index) => ( 192 |
201 | ))} 202 | 203 | ); 204 | }, 205 | width: 300, 206 | }, 207 | { 208 | key: 'sizes', 209 | dataIndex: 'subItems', 210 | title: 'Sizes', 211 | render: (items: SubProductModel[]) => ( 212 | 213 | {items.length > 0 && 214 | items.map((item, index) => ( 215 | {item.size} 216 | ))} 217 | 218 | ), 219 | width: 150, 220 | }, 221 | { 222 | key: 'price', 223 | dataIndex: 'subItems', 224 | title: 'Price', 225 | render: (items: SubProductModel[]) => ( 226 | {getMinMaxValues(items)} 227 | ), 228 | width: 200, 229 | }, 230 | { 231 | key: 'stock', 232 | dataIndex: 'subItems', 233 | title: 'Stock', 234 | render: (items: SubProductModel[]) => 235 | items.reduce((a, b) => a + b.qty, 0), 236 | align: 'right', 237 | width: 100, 238 | }, 239 | { 240 | key: 'actions', 241 | title: 'Actions', 242 | dataIndex: '', 243 | fixed: 'right', 244 | width: 150, 245 | render: (item: ProductModel) => ( 246 | 247 | 248 | 360 | 361 | 362 | {selectedRowKeys.length} items selected 363 | 364 | {selectedRowKeys.length < total && ( 365 | 368 | )} 369 | 370 | )} 371 |
372 | 373 |
374 | 375 | {isFilting && ( 376 | 386 | )} 387 | setSearchKey(val.target.value)} 390 | onSearch={handleSearchProducts} 391 | placeholder='Search' 392 | allowClear 393 | /> 394 | ( 396 | handleFilterProducts(vals)} 399 | /> 400 | )}> 401 | 402 | 403 | 404 | 405 | 406 |
407 | 408 |
record._id} 410 | pagination={{ 411 | showSizeChanger: true, 412 | onShowSizeChange: (current, size) => { 413 | // console.log(current, size); 414 | // console.log('size'); 415 | }, 416 | total, 417 | onChange(page, pageSize) { 418 | setPage(page); 419 | setPageSize(pageSize); 420 | }, 421 | showQuickJumper: false, 422 | }} 423 | rowSelection={rowSelection} 424 | dataSource={products} 425 | columns={columns} 426 | loading={isLoading} 427 | scroll={{ 428 | x: '100%', 429 | }} 430 | bordered 431 | size='small' 432 | /> 433 | 434 | { 438 | setProductSelected(undefined); 439 | setIsVisibleAddSubProduct(false); 440 | }} 441 | onAddNew={async (val) => {}} 442 | /> 443 | 444 | ); 445 | }; 446 | 447 | export default Inventories; 448 | -------------------------------------------------------------------------------- /src/screens/inventories/ProductDetail.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { useEffect, useState } from 'react'; 4 | import { useSearchParams } from 'react-router-dom'; 5 | import handleAPI from '../../apis/handleAPI'; 6 | import { ProductModel, SubProductModel } from '../../models/Products'; 7 | import { 8 | Empty, 9 | Space, 10 | Spin, 11 | Typography, 12 | Table, 13 | Avatar, 14 | Tag, 15 | Button, 16 | Modal, 17 | message, 18 | } from 'antd'; 19 | import { ColumnProps } from 'antd/es/table'; 20 | import { Edit2, Trash } from 'iconsax-react'; 21 | import { VND } from '../../utils/handleCurrency'; 22 | import { colors } from '../../constants/colors'; 23 | import { AddSubProductModal } from '../../modals'; 24 | 25 | const ProductDetail = () => { 26 | const [isLoading, setIsLoading] = useState(false); 27 | const [productDetail, setProductDetail] = useState(); 28 | const [subProducts, setSubProducts] = useState([]); 29 | const [productSelected, setProductSelected] = useState(); 30 | const [isVisibleAddSubProduct, setIsVisibleAddSubProduct] = useState(false); 31 | const [subProductSelected, setSubProductSelected] = 32 | useState(); 33 | 34 | const [searchParams] = useSearchParams(); 35 | 36 | const id = searchParams.get('id'); 37 | 38 | useEffect(() => { 39 | if (id) { 40 | getProductDetail(); 41 | } 42 | }, [id]); 43 | 44 | useEffect(() => { 45 | setProductSelected(productDetail); 46 | }, [productDetail]); 47 | 48 | const getProductDetail = async () => { 49 | const api = `/products/detail?id=${id}`; 50 | 51 | setIsLoading(true); 52 | try { 53 | const res = await handleAPI(api); 54 | setProductDetail(res.data.product); 55 | setSubProducts(res.data.subProducts); 56 | } catch (error) { 57 | console.log(error); 58 | } finally { 59 | setIsLoading(false); 60 | } 61 | }; 62 | 63 | const handleRemoveSubProduct = async (id: string) => { 64 | const api = `/products/remove-sub-product?id=${id}&isSoftDelete=true`; 65 | 66 | try { 67 | await handleAPI(api, undefined, 'delete'); 68 | message.success('sub product removed!!!'); 69 | 70 | // update state 71 | const items = [...subProducts]; 72 | const index = items.findIndex((element) => element._id === id); 73 | 74 | if (index - 1) { 75 | items.splice(index, 1); 76 | } 77 | 78 | setSubProducts(items); 79 | 80 | // call api again 81 | } catch (error) { 82 | console.log(error); 83 | } 84 | }; 85 | 86 | const columns: ColumnProps[] = [ 87 | { 88 | key: 'images', 89 | dataIndex: 'images', 90 | title: 'Images', 91 | render: (imgs: string[]) => ( 92 | 93 | {imgs.length > 0 && imgs.map((img) => )} 94 | 95 | ), 96 | }, 97 | { 98 | title: 'Size', 99 | key: 'size', 100 | dataIndex: 'size', 101 | render: (size: string) => {size}, 102 | align: 'center', 103 | }, 104 | { 105 | title: 'Color', 106 | key: 'color', 107 | dataIndex: 'color', 108 | render: (color: string) => {color}, 109 | align: 'center', 110 | }, 111 | { 112 | key: 'price', 113 | title: 'Price', 114 | dataIndex: 'price', 115 | render: (price: number) => VND.format(price), 116 | align: 'right', 117 | }, 118 | { 119 | key: 'discount', 120 | title: 'Discount', 121 | dataIndex: 'discount', 122 | render: (discount: number) => (discount ? VND.format(discount) : null), 123 | align: 'right', 124 | }, 125 | { 126 | key: 'stock', 127 | title: 'stock', 128 | dataIndex: 'qty', 129 | render: (qty: number) => qty.toLocaleString(), 130 | align: 'right', 131 | }, 132 | { 133 | key: 'actions', 134 | dataIndex: '', 135 | render: (item: SubProductModel) => ( 136 | 137 | 179 | 180 | 181 |
182 |
183 | 184 | {productDetail && ( 185 | { 189 | setProductSelected(undefined); 190 | setIsVisibleAddSubProduct(false); 191 | }} 192 | subProduct={subProductSelected} 193 | onAddNew={async (val) => { 194 | await getProductDetail(); 195 | // setSubProducts([...subProducts, val]); 196 | }} 197 | /> 198 | )} 199 | 200 | ) : ( 201 | 202 | ); 203 | }; 204 | 205 | export default ProductDetail; 206 | -------------------------------------------------------------------------------- /src/screens/orther/AddOrder.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { Button, Card, ColorPicker, Table } from 'antd'; 4 | import React, { useState } from 'react'; 5 | import { AddSubProductModal } from '../../modals'; 6 | import { SubProductModel } from '../../models/Products'; 7 | import { ColumnProps } from 'antd/es/table'; 8 | import { authSeletor } from '../../redux/reducers/authReducer'; 9 | import { useSelector } from 'react-redux'; 10 | import handleAPI from '../../apis/handleAPI'; 11 | 12 | interface ItemProps extends SubProductModel { 13 | product: { 14 | label: string; 15 | value: string; 16 | }; 17 | } 18 | 19 | const AddOrder = () => { 20 | const [isVisibleAddSubProduct, setIsVisibleAddSubProduct] = useState(false); 21 | const [subProducts, setSubProducts] = useState([]); 22 | const [isLoading, setIsLoading] = useState(false); 23 | 24 | const auth = useSelector(authSeletor); 25 | 26 | const columns: ColumnProps[] = [ 27 | { 28 | key: '#', 29 | align: 'center', 30 | title: '#', 31 | dataIndex: '', 32 | render: (text, record, index) => index + 1, 33 | width: 50, 34 | }, 35 | { 36 | key: 'title', 37 | title: 'Title', 38 | dataIndex: 'product', 39 | render: (text, record) => record.product?.label, 40 | }, 41 | { 42 | key: 'color', 43 | dataIndex: 'color', 44 | title: 'Color', 45 | render: (val) => ( 46 | 47 | ), 48 | }, 49 | { 50 | key: 'size', 51 | dataIndex: 'size', 52 | title: 'Size', 53 | }, 54 | { 55 | key: 'quantity', 56 | dataIndex: 'qty', 57 | title: 'Quantity', 58 | }, 59 | { 60 | key: 'price', 61 | dataIndex: 'price', 62 | title: 'Price', 63 | }, 64 | { 65 | key: 'cost', 66 | dataIndex: 'cost', 67 | title: 'Cost', 68 | }, 69 | { 70 | key: 'total', 71 | dataIndex: '', 72 | title: 'Total', 73 | render: (text, record) => record.qty * record.price, 74 | }, 75 | ]; 76 | 77 | const handleAddOrder = async () => { 78 | // const api = `/orders/add`; 79 | 80 | // /* 81 | // { 82 | // product_id: { type: String, required: true, ref: 'Product' }, 83 | // total: { type: Number, required: true }, 84 | // price: { type: Number, required: true }, 85 | // quantity: { type: Number, required: true }, 86 | // subProduct_id: { type: String, required: true, ref: 'SubProduct' }, 87 | // cost: { type: Number, required: true }, 88 | // status: { type: String, required: true }, 89 | // }, 90 | // */ 91 | // const data = { 92 | // user_id: auth._id, 93 | // items: subProducts.map((item: any) => { 94 | // item.product_id = item.productId; 95 | // item.total = item.qty * item.cost 96 | // item.status = 'pending'; 97 | 98 | // delete item.product; 99 | // delete item.productId; 100 | 101 | // return item; 102 | // }), 103 | // }; 104 | 105 | // console.log(data); 106 | 107 | // tạo 1 subproduct mới 108 | // lấy id subproduct 109 | // tạo order mới 110 | 111 | setIsLoading(true); 112 | try { 113 | const promises = subProducts.map(async (item: any) => { 114 | delete item.product; 115 | const api = `/products/add-sub-product`; 116 | const res = await handleAPI(api, item, 'post'); 117 | return { 118 | ...item, 119 | subProduct_id: res.data._id, 120 | total: item.qty * item.cost, 121 | status: 'done', 122 | quantity: item.qty, 123 | product_id: item.productId, 124 | }; 125 | }); 126 | 127 | const items = await Promise.all(promises); 128 | 129 | const data = { 130 | items, 131 | user_id: auth._id, 132 | total: items.reduce((acc, item) => acc + item.total, 0), 133 | }; 134 | 135 | const result = await handleAPI(`/orders/add`, data, 'post'); 136 | 137 | console.log(result); 138 | } catch (error) { 139 | console.log(error); 140 | } finally { 141 | setIsLoading(false); 142 | } 143 | }; 144 | 145 | return ( 146 |
147 | setIsVisibleAddSubProduct(true)}> 153 | Add product 154 | 155 | }> 156 |
162 | 163 |
164 | 171 |
172 | 173 | 174 | { 178 | setIsVisibleAddSubProduct(false); 179 | }} 180 | onAddNew={async (val: any) => { 181 | setIsVisibleAddSubProduct(false); 182 | 183 | const index = subProducts.findIndex( 184 | (element) => 185 | element.product.value === val.product.value && 186 | element.color === val.color && 187 | element.size === val.size 188 | ); 189 | 190 | if (index !== -1) { 191 | const item: ItemProps = subProducts[index]; 192 | item.qty += val.qty; 193 | item.price = val.price; 194 | item.cost = val.cost; 195 | 196 | const newSubProducts = [...subProducts]; 197 | newSubProducts[index] = item; 198 | setSubProducts(newSubProducts); 199 | } else { 200 | setSubProducts([...subProducts, val]); 201 | } 202 | }} 203 | /> 204 | 205 | ); 206 | }; 207 | 208 | export default AddOrder; 209 | -------------------------------------------------------------------------------- /src/screens/orther/index.tsx: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { useEffect, useState } from 'react'; 4 | import handleAPI from '../../apis/handleAPI'; 5 | import { 6 | Button, 7 | Card, 8 | ColorPicker, 9 | DatePicker, 10 | Radio, 11 | Space, 12 | Statistic, 13 | Table, 14 | Typography, 15 | } from 'antd'; 16 | import { OrderModel } from '../../models/OrderModel'; 17 | import { ColumnProps } from 'antd/es/table'; 18 | import { DateTime } from '../../utils/dateTime'; 19 | import { useNavigate } from 'react-router-dom'; 20 | import { StatisticComponent } from '../../components'; 21 | 22 | const Orders = () => { 23 | const [loading, setLoading] = useState(false); 24 | const [page, setPage] = useState(1); 25 | const [limit, setLimit] = useState(20); 26 | const [total, setTotal] = useState(0); 27 | const [api, setApi] = useState(''); 28 | const [orders, setOrders] = useState([]); 29 | const [dateSelected, setDateSelected] = useState< 30 | 'day' | 'week' | 'month' | 'year' 31 | >('month'); 32 | const [dates, setDates] = useState< 33 | | { 34 | start: Date; 35 | end: Date; 36 | } 37 | | undefined 38 | >(); 39 | 40 | const navigate = useNavigate(); 41 | 42 | useEffect(() => { 43 | if (dateSelected) { 44 | let startDate = new Date(); 45 | let endDate = new Date(); 46 | switch (dateSelected) { 47 | case 'week': 48 | startDate.setDate(startDate.getDate() - 7); 49 | endDate.setDate(endDate.getDate()); 50 | break; 51 | 52 | case 'month': 53 | startDate.setDate(startDate.getDate() - 30); 54 | endDate.setDate(endDate.getDate()); 55 | break; 56 | 57 | case 'year': 58 | startDate.setDate(startDate.getDate() - 365); 59 | endDate.setDate(endDate.getDate()); 60 | break; 61 | 62 | default: 63 | setDates({ 64 | start: new Date(new Date().setHours(0, 0, 0, 0)), 65 | end: new Date(new Date().setHours(23, 59, 59, 999)), 66 | }); 67 | break; 68 | } 69 | 70 | setDates({ start: startDate, end: endDate }); 71 | } else { 72 | setDates(undefined); 73 | } 74 | }, [dateSelected]); 75 | 76 | useEffect(() => { 77 | setApi( 78 | `/orders?page=${page}&limit=${limit}${ 79 | dates 80 | ? `&start=${dates?.start.toISOString()}&end=${dates?.end.toISOString()}` 81 | : '' 82 | }` 83 | ); 84 | }, [page, limit, dates]); 85 | 86 | useEffect(() => { 87 | api && getOrders(api); 88 | }, [api]); 89 | 90 | const getOrders = async (url: string) => { 91 | setLoading(true); 92 | // console.log(url); 93 | try { 94 | const res: any = await handleAPI(url); 95 | 96 | setOrders(res.data); 97 | setTotal(res.total); 98 | } catch (error) { 99 | console.log(error); 100 | } finally { 101 | setLoading(false); 102 | } 103 | }; 104 | 105 | const columns: ColumnProps[] = [ 106 | { 107 | key: '#', 108 | title: '#', 109 | dataIndex: '', 110 | render: (text, record, index) => index + 1, 111 | }, 112 | { 113 | key: 'product', 114 | title: 'Product', 115 | dataIndex: 'product', 116 | render: (text, record) => record.product?.title, 117 | }, 118 | { 119 | key: 'subProduct', 120 | title: 'Sub Product', 121 | dataIndex: 'subProduct', 122 | render: (text, record) => ( 123 | 124 | ), 125 | }, 126 | { 127 | key: 'quantity', 128 | title: 'Quantity', 129 | dataIndex: 'quantity', 130 | }, 131 | { 132 | key: 'cost', 133 | title: 'Cost', 134 | dataIndex: 'cost', 135 | }, 136 | { 137 | key: 'price', 138 | title: 'Price', 139 | dataIndex: 'price', 140 | }, 141 | { 142 | key: 'total', 143 | title: 'Total', 144 | dataIndex: 'total', 145 | }, 146 | { 147 | key: 'status', 148 | title: 'Status', 149 | dataIndex: 'status', 150 | }, 151 | { 152 | key: 'createdAt', 153 | title: 'Created At', 154 | dataIndex: 'createdAt', 155 | render: (text, record) => DateTime.CalendarDate(record.createdAt), 156 | }, 157 | ]; 158 | 159 | const renderStatisticItem = ({ 160 | title, 161 | value, 162 | total, 163 | label, 164 | desc, 165 | color, 166 | }: { 167 | title: string; 168 | value: string; 169 | total?: string; 170 | label: string; 171 | desc?: string; 172 | color?: string; 173 | }) => { 174 | return ( 175 |
176 | 179 | {title} 180 | 181 |
182 | 183 | {value} 184 | 185 | {total && ( 186 | 187 | {total} 188 | 189 | )} 190 |
191 |
192 | {label} 193 | {desc && {desc}} 194 |
195 |
196 | ); 197 | }; 198 | 199 | const flatArray = (arr: OrderModel[]) => { 200 | const items: string[] = []; 201 | 202 | arr.map((item) => { 203 | !items.includes(item.product_id) && items.push(item.product_id); 204 | }); 205 | 206 | return items; 207 | }; 208 | return ( 209 |
210 |
211 | 212 | setDateSelected(val.target.value)} 214 | value={dateSelected}> 215 | setDateSelected('day')}> 216 | Day 217 | 218 | setDateSelected('week')}> 219 | Week 220 | 221 | setDateSelected('month')}> 224 | Month 225 | 226 | setDateSelected('year')}> 227 | Year 228 | 229 | 230 | { 234 | if (vals && vals.length === 2) { 235 | setDates({ start: vals[0].toDate(), end: vals[1].toDate() }); 236 | } 237 | }} 238 | /> 239 | 240 |
241 | 242 |
243 | {renderStatisticItem({ 244 | title: 'Products', 245 | value: `${orders.length > 0 ? flatArray(orders).length : 0} `, 246 | label: `Last ${30} days`, 247 | })} 248 | {renderStatisticItem({ 249 | title: 'Sub Products', 250 | value: orders.reduce((a, b) => a + b.quantity, 0).toLocaleString(), 251 | total: orders.reduce((a, b) => a + b.total, 0).toLocaleString(), 252 | label: `Last ${30} days`, 253 | })} 254 | {renderStatisticItem({ 255 | title: 'Done', 256 | value: orders 257 | .filter((element) => element.status === 'done') 258 | .reduce((a, b) => a + b.quantity, 0) 259 | .toLocaleString(), 260 | total: orders 261 | .filter((element) => element.status === 'done') 262 | .reduce((a, b) => a + b.total, 0) 263 | .toLocaleString(), 264 | label: `Last ${30} days`, 265 | })} 266 | {renderStatisticItem({ 267 | title: 'Waiting', 268 | value: orders 269 | .filter((element) => element.status === 'waiting') 270 | .reduce((a, b) => a + b.quantity, 0) 271 | .toLocaleString(), 272 | total: orders 273 | .filter((element) => element.status === 'waiting') 274 | .reduce((a, b) => a + b.total, 0) 275 | .toLocaleString(), 276 | label: `Last ${30} days`, 277 | })} 278 |
279 |
280 | navigate('/orders/add-new')}> 284 | Add Order 285 | 286 | }> 287 |
setPage(page), 298 | }} 299 | /> 300 | 301 | 302 | ); 303 | }; 304 | 305 | export default Orders; 306 | -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/utils/add0toNumber.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export const add0toNumber = (num: number) => `${num > 9 ? num : `0${num}`}`; 4 | -------------------------------------------------------------------------------- /src/utils/dateTime.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { add0toNumber } from './add0toNumber'; 4 | 5 | export class DateTime { 6 | static CalendarDate = (val: any) => { 7 | const date = new Date(val); 8 | 9 | // YYYY/MM/DD 10 | return `${date.getFullYear()}-${add0toNumber( 11 | date.getMonth() + 1 12 | )}-${add0toNumber(date.getDate())}`; 13 | }; 14 | 15 | static getShortDate = (val: any) => { 16 | const date = new Date(val); 17 | return `${add0toNumber(date.getDate())}/${add0toNumber( 18 | date.getMonth() + 1 19 | )}`; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/formatNumber.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export const formatNumber = (num: string) => 4 | num.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); 5 | 6 | export class FormatCurrency { 7 | static VND = new Intl.NumberFormat(`vi-VN`, { 8 | style: 'currency', 9 | currency: 'VND', 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /src/utils/getTreeValues.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export const getTreeValues = (data: any[], isSelect?: boolean) => { 4 | const values: any = []; 5 | const items = data.filter((element) => !element.parentId); 6 | const newItems = items.map((item) => 7 | isSelect 8 | ? { 9 | label: item.title, 10 | value: item._id, 11 | } 12 | : { ...item, key: item._id } 13 | ); 14 | 15 | newItems.forEach((item) => { 16 | const children = changeMenu( 17 | data, 18 | isSelect ? item.value : item._id, 19 | isSelect ?? false 20 | ); 21 | values.push({ 22 | ...item, 23 | children, 24 | }); 25 | }); 26 | 27 | return values; 28 | }; 29 | 30 | const changeMenu = (data: any[], id: string, isSelect: boolean) => { 31 | const items: any = []; 32 | const datas = data.filter((element) => element.parentId === id); 33 | 34 | datas.forEach((val) => 35 | items.push( 36 | isSelect 37 | ? { 38 | label: val.title, 39 | value: val._id, 40 | children: changeMenu(data, val._id, isSelect), 41 | } 42 | : { 43 | ...val, 44 | key: val._id, 45 | children: changeMenu(data, val._id, isSelect), 46 | } 47 | ) 48 | ); 49 | return items; 50 | }; 51 | -------------------------------------------------------------------------------- /src/utils/handleCurrency.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export const VND = new Intl.NumberFormat('vi-VN', { 4 | style: 'currency', 5 | currency: 'VND', 6 | }); 7 | -------------------------------------------------------------------------------- /src/utils/handleExportExcel.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { utils, writeFileXLSX } from 'xlsx'; 4 | 5 | export const hanldExportExcel = async (data: any[], name?: string) => { 6 | const ws = utils.json_to_sheet(data); 7 | const wb = utils.book_new(); 8 | 9 | utils.book_append_sheet(wb, ws, `data`); 10 | 11 | writeFileXLSX(wb, `${name ? name : `Kanban-data-${Date.now()}`}.xlsx`); 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/replaceName.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export const replaceName = (str: string) => { 4 | return str 5 | .normalize('NFD') 6 | .toLocaleLowerCase() 7 | .replace(/[\u0300-\u036f]/g, '') 8 | .replace(/đ/g, 'd') 9 | .replace(/Đ/g, 'D') 10 | .replace(/ /g, '-') 11 | .replace(/[:!@#$%^&*()?;/]/g, ''); 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/uploadFile.ts: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import { UploadProps } from 'antd'; 4 | import { storage } from '../firebase/firebaseConfig'; 5 | import { replaceName } from './replaceName'; 6 | import { getDownloadURL, ref, uploadBytes } from 'firebase/storage'; 7 | import Resizer from 'react-image-file-resizer'; 8 | 9 | export const uploadFile = async (file: any) => { 10 | const newFile: any = await handleResize(file); 11 | 12 | const filename = replaceName(newFile.name); 13 | 14 | const storageRef = ref(storage, `images/${filename}`); 15 | 16 | const res = await uploadBytes(storageRef, newFile); 17 | 18 | if (res) { 19 | if (res.metadata.size === newFile.size) { 20 | return getDownloadURL(storageRef); 21 | } else { 22 | return 'Uploading'; 23 | } 24 | } else { 25 | return 'Error upload'; 26 | } 27 | }; 28 | 29 | export const handleResize = (file: any) => 30 | new Promise((resolve) => { 31 | Resizer.imageFileResizer( 32 | file, 33 | 1080, 34 | 720, 35 | 'JPEG', 36 | 80, 37 | 0, 38 | (newfile) => { 39 | newfile && resolve(newfile); 40 | }, 41 | 'file' 42 | ); 43 | }); 44 | 45 | export const handleChangeFile: UploadProps['onChange'] = ({ 46 | fileList: newFileList, 47 | }) => { 48 | const items = newFileList.map((item) => 49 | item.originFileObj 50 | ? { 51 | ...item, 52 | url: item.originFileObj 53 | ? URL.createObjectURL(item.originFileObj) 54 | : '', 55 | status: 'done', 56 | } 57 | : { ...item } 58 | ); 59 | 60 | return items; 61 | }; 62 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src", "**/*.ts", "**/*.tsx"] 20 | } 21 | --------------------------------------------------------------------------------