├── .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 |
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 |
123 |
129 |
130 | <>
131 | {selectDatas.colors && selectDatas.colors.length > 0 && (
132 |
133 | {selectDatas.colors.map((color) => (
134 |
166 | ))}
167 |
168 | )}
169 | >
170 |
171 |
172 |
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 = ;
23 | break;
24 | default:
25 | content = (
26 |
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 |
110 |
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 | }>
159 | Filters
160 |
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 |
144 |
145 |
146 |
147 |
148 |
149 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 | );
199 | };
200 |
201 | export default AddPromotion;
202 |
--------------------------------------------------------------------------------
/src/modals/AddSubProductModal.tsx:
--------------------------------------------------------------------------------
1 | /** @format */
2 |
3 | import {
4 | ColorPicker,
5 | Form,
6 | Image,
7 | Input,
8 | InputNumber,
9 | message,
10 | Modal,
11 | Select,
12 | Typography,
13 | Upload,
14 | UploadProps,
15 | } from 'antd';
16 | import { useEffect, useState } from 'react';
17 | import handleAPI from '../apis/handleAPI';
18 | import { colors } from '../constants/colors';
19 | import { ProductModel, SubProductModel } from '../models/Products';
20 | import { uploadFile } from '../utils/uploadFile';
21 | import { useSelector } from 'react-redux';
22 | import { authSeletor } from '../redux/reducers/authReducer';
23 | import { SelectModel } from '../models/FormModel';
24 | import { get } from 'http';
25 |
26 | interface Props {
27 | visible: boolean;
28 | onClose: () => void;
29 | product?: ProductModel;
30 | onAddNew: (val: SubProductModel) => void;
31 | subProduct?: SubProductModel;
32 | }
33 |
34 | const AddSubProductModal = (props: Props) => {
35 | const { visible, onClose, product, onAddNew, subProduct } = props;
36 |
37 | const [isLoading, setIsLoading] = useState(false);
38 | const [fileList, setFileList] = useState([]);
39 | const [previewOpen, setPreviewOpen] = useState(false);
40 | const [previewImage, setPreviewImage] = useState('');
41 | const [options, setOptions] = useState();
42 | const [form] = Form.useForm();
43 |
44 | const auth = useSelector(authSeletor);
45 |
46 | useEffect(() => {
47 | form.setFieldValue('color', colors.primary500);
48 | }, []);
49 |
50 | useEffect(() => {
51 | if (!product) {
52 | getProductOptions();
53 | }
54 | }, [product]);
55 |
56 | useEffect(() => {
57 | if (subProduct) {
58 | form.setFieldsValue(subProduct);
59 |
60 | if (subProduct.images && subProduct.images.length > 0) {
61 | const items = subProduct.images.map((item) => ({
62 | url: item,
63 | }));
64 |
65 | setFileList(items);
66 | }
67 | }
68 | }, [subProduct]);
69 |
70 | const handleAddSubproduct = async (values: any) => {
71 | const data: any = {};
72 |
73 | for (const i in values) {
74 | data[i] = values[i] ?? '';
75 | }
76 | data.productId = product ? product._id : values.productId;
77 |
78 | if (!data.productId) {
79 | message.error('Please select product');
80 | return;
81 | } else {
82 | if (data.color) {
83 | data.color =
84 | typeof data.color === 'string'
85 | ? data.color
86 | : data.color.toHexString();
87 | }
88 |
89 | if (fileList.length > 0) {
90 | const promises = fileList.map(async (file) => {
91 | const url = await uploadFile(file.originFileObj);
92 | return url;
93 | });
94 |
95 | const urls = await Promise.all(promises);
96 |
97 | data.images = urls;
98 | }
99 |
100 | if (!product) {
101 | onAddNew({
102 | ...data,
103 | product: options?.find((item) => item.value === data.productId),
104 | });
105 | handleCancel();
106 | } else {
107 | setIsLoading(true);
108 | await createSubProduct(data);
109 | }
110 | }
111 | };
112 |
113 | const createSubProduct = async (data: any) => {
114 | const api = `/products/${
115 | subProduct ? `update-sub-product?id=${subProduct._id}` : 'add-sub-product'
116 | }`;
117 |
118 | try {
119 | const res = await handleAPI(api, data, subProduct ? 'put' : 'post');
120 | // await handleAddOrder({ ...data, subProduct_id: res?.data._id });
121 | onAddNew(res.data);
122 | handleCancel();
123 | } catch (error) {
124 | console.log(error);
125 | } finally {
126 | setIsLoading(false);
127 | }
128 | };
129 |
130 | const handleCancel = () => {
131 | form.resetFields();
132 | onClose();
133 | };
134 |
135 | const handleChange: UploadProps['onChange'] = ({ fileList: newFileList }) => {
136 | const items = newFileList.map((item) =>
137 | item.originFileObj
138 | ? {
139 | ...item,
140 | url: item.originFileObj
141 | ? URL.createObjectURL(item.originFileObj)
142 | : '',
143 | status: 'done',
144 | }
145 | : { ...item }
146 | );
147 |
148 | setFileList(items);
149 | };
150 |
151 | const getProductOptions = async () => {
152 | const api = `/products/get-product-options`;
153 |
154 | try {
155 | const res = await handleAPI(api);
156 | setOptions(res.data);
157 | } catch (error) {
158 | console.log(error);
159 | }
160 | };
161 |
162 | return (
163 | form.submit()}
169 | okButtonProps={{
170 | loading: isLoading,
171 | }}>
172 | {product?.title}
173 |
181 |
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 |
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 |
113 | ),
114 | },
115 | ];
116 |
117 | return (
118 |
119 |
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 | {
110 | setSupplierSelected(item);
111 | setIsVisibleModalAddNew(true);
112 | }}
113 | icon={}
114 | />
115 |
116 |
118 | confirm({
119 | title: 'Comfirm',
120 | content: 'Are you sure you want to remove this supplier?',
121 | onOk: () => removeSuppiler(item._id),
122 | })
123 | }
124 | type='text'
125 | icon={}
126 | />
127 |
128 | )}
129 | />
130 |
131 | {
134 | supplierSelected && getSuppliers();
135 | setSupplierSelected(undefined);
136 | setIsVisibleModalAddNew(false);
137 | }}
138 | onAddNew={(val) => setSuppliers([...suppliers, val])}
139 | supplier={supplierSelected}
140 | />
141 |
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 |
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 | form.submit()}
117 | type='primary'
118 | style={{
119 | width: '100%',
120 | }}
121 | size='large'>
122 | Login
123 |
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 |
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 | form.submit()}
117 | type='primary'
118 | style={{
119 | width: '100%',
120 | }}
121 | size='large'>
122 | Sing up
123 |
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 |
88 | }>
89 | Google
90 |
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 | setCategorySelected(item)}
84 | icon={}
85 | type='text'
86 | />
87 |
88 |
89 |
91 | confirm({
92 | title: 'Confirm',
93 | content: 'What are you sure you want to remove this item?',
94 | onOk: async () => handleRemove(item._id),
95 | })
96 | }
97 | icon={}
98 | type='text'
99 | />
100 |
101 |
102 | ),
103 | align: 'right',
104 | },
105 | ];
106 |
107 | const handleRemove = async (id: string) => {
108 | const api = `/products/delete-category?id=${id}`;
109 |
110 | try {
111 | await handleAPI(api, undefined, 'delete');
112 | setCategories((categories) =>
113 | categories.filter((element) => element._id !== id)
114 | );
115 | message.success('Deleted!!');
116 | } catch (error: any) {
117 | console.log(error);
118 | message.error(error.message);
119 | }
120 | };
121 |
122 | return isLoading ? (
123 |
124 | ) : (
125 |
126 |
127 |
128 |
129 |
130 | setCategorySelected(undefined)}
132 | seleted={categorySelected}
133 | values={treeValues}
134 | onAddNew={async (val) => {
135 | if (categorySelected) {
136 | const items = [...categories];
137 | const index = items.findIndex(
138 | (element) => element._id === categorySelected._id
139 | );
140 | if (index !== -1) {
141 | items[index] = val;
142 | }
143 |
144 | setCategories(items);
145 | setCategorySelected(undefined);
146 |
147 | await getCategories(`/products/get-categories`, true);
148 | } else {
149 | getCategories(
150 | `/products/get-categories?page=${page}&pageSize=${pageSize}`
151 | );
152 | }
153 | }}
154 | />
155 |
156 |
157 |
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 |
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 | }
250 | type='text'
251 | onClick={() => {
252 | setProductSelected(item);
253 | setIsVisibleAddSubProduct(true);
254 | }}
255 | />
256 |
257 |
258 | }
260 | type='text'
261 | onClick={() => navigate(`/inventory/add-product?id=${item._id}`)}
262 | />
263 |
264 |
265 | }
267 | type='text'
268 | onClick={() =>
269 | confirm({
270 | title: 'Confirm?',
271 | content: 'Are you sure you want to delete this item?',
272 | onCancel: () => console.log('cancel'),
273 | onOk: () => hanleRemoveProduct(item._id),
274 | })
275 | }
276 | />
277 |
278 |
279 | ),
280 | align: 'right',
281 | },
282 | ];
283 |
284 | const handleSelectAllProduct = async () => {
285 | try {
286 | const res = await handleAPI('/products');
287 |
288 | const items = res.data.items;
289 |
290 | if (items.length > 0) {
291 | const keys = items.map((item: any) => item._id);
292 |
293 | setSelectedRowKeys(keys);
294 | }
295 | } catch (error) {
296 | console.log(error);
297 | }
298 | };
299 |
300 | const handleSearchProducts = async () => {
301 | const key = replaceName(searchKey);
302 | setPage(1);
303 | const api = `/products?title=${key}&page=${page}&pageSize=${pageSize}`;
304 | setIsLoading(true);
305 | try {
306 | const res = await handleAPI(api);
307 |
308 | setProducts(res.data.items);
309 | setTotal(res.data.total);
310 | } catch (error) {
311 | console.log(error);
312 | } finally {
313 | setIsLoading(false);
314 | }
315 | };
316 |
317 | const handleFilterProducts = async (vals: FilterProductValue) => {
318 | const api = `/products/filter-products`;
319 | setIsFilting(true);
320 | try {
321 | // console.log(vals);
322 | const res = await handleAPI(api, vals, 'post');
323 | setTotal(res.data.totalItems);
324 | setProducts(res.data.items);
325 | } catch (error) {
326 | console.log(error);
327 | }
328 | };
329 |
330 | return (
331 |
332 |
333 |
334 | Product
335 |
336 |
337 | {selectedRowKeys.length > 0 && (
338 |
339 |
340 |
342 | confirm({
343 | title: 'Confirm?',
344 | content: 'Are you sure you want to delete this item?',
345 | onCancel: () => {
346 | setSelectedRowKeys([]);
347 | },
348 | onOk: () => {
349 | selectedRowKeys.forEach(
350 | async (key) => await hanleRemoveProduct(key)
351 | );
352 | },
353 | })
354 | }
355 | danger
356 | type='text'
357 | icon={}>
358 | Delete
359 |
360 |
361 |
362 | {selectedRowKeys.length} items selected
363 |
364 | {selectedRowKeys.length < total && (
365 |
366 | Select all
367 |
368 | )}
369 |
370 | )}
371 |
372 |
373 |
374 |
375 | {isFilting && (
376 | {
378 | setPage(1);
379 | await getProducts(
380 | `/products?page=${page}&pageSize=${pageSize}`
381 | );
382 | setIsFilting(false);
383 | }}>
384 | Clear filter values
385 |
386 | )}
387 | setSearchKey(val.target.value)}
390 | onSearch={handleSearchProducts}
391 | placeholder='Search'
392 | allowClear
393 | />
394 | (
396 | handleFilterProducts(vals)}
399 | />
400 | )}>
401 | }>Filter
402 |
403 |
404 | Add Product
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 | {
140 | setSubProductSelected(item);
141 | setIsVisibleAddSubProduct(true);
142 | }}
143 | icon={}
144 | />
145 |
147 | Modal.confirm({
148 | title: 'Confirm',
149 | content:
150 | 'Are you sure you want to remove this sub product item?',
151 | onOk: async () => await handleRemoveSubProduct(item._id),
152 | })
153 | }
154 | type='text'
155 | danger
156 | icon={}
157 | />
158 |
159 | ),
160 | align: 'right',
161 | fixed: 'right',
162 | },
163 | ];
164 |
165 | return isLoading ? (
166 |
167 | ) : productDetail ? (
168 |
169 |
170 |
171 | {productDetail?.title}
172 |
173 |
174 | setIsVisibleAddSubProduct(true)}
176 | type='primary'>
177 | Add sub product
178 |
179 |
180 |
181 |
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 |
169 | Submit
170 |
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 |
--------------------------------------------------------------------------------