Procházet zdrojové kódy

+Admin Good Actions

ilya_shyian před 2 roky
rodič
revize
8e1960b760

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 29083
package-lock.json


+ 3 - 2
package.json

@@ -1,7 +1,7 @@
 {
   "name": "diploma",
   "version": "0.1.0",
-  "proxy":"http://127.0.0.1:8000/api",
+  "proxy": "http://127.0.0.1:8000",
   "private": true,
   "dependencies": {
     "@dnd-kit/core": "^5.0.3",
@@ -24,6 +24,7 @@
     "react-scripts": "5.0.1",
     "react-select": "^5.3.2",
     "react-sortable-hoc": "^2.0.0",
+    "react-sortable-tree": "^2.8.0",
     "redux": "^4.2.0",
     "redux-devtools-extension": "^2.13.9",
     "redux-thunk": "^2.4.1",
@@ -31,7 +32,7 @@
     "yup": "^0.32.11"
   },
   "engines": {
-    "node": "16.5.x",
+    "node": "18.4.x",
     "npm": "7.19.x"
   },
   "scripts": {

+ 23 - 15
src/actions/actionCatAll.js

@@ -7,21 +7,29 @@ export const actionCatAll =
         dispatch(
             actionPromise(
                 promiseName,
-                fetch(`${backendURL}/categories/?limit=${limit}&skip=${skip}${orderBy && `&orderBy=` + orderBy}`, {
-                    method: 'GET',
-                    mode: 'cors',
-                    headers: {
-                        'Content-Type': 'application/json',
-                        accept: 'application/json',
-                        ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
-                    },
-                })
-                    .then((res) => res.json())
-                    .then((data) => {
-                        if (data.errors) {
-                            throw new Error(JSON.stringify(data.errors));
-                        } else return data.data;
-                    })
+                gql(
+                    `query CatAll($query:String){
+                        CategoryFind(query: $query){
+                            _id name
+                            parent{
+                                _id, name
+                            }
+                            subcategories{
+                                _id name
+                            }
+                        }
+                    }`,
+                    {
+                        query: JSON.stringify([
+                            {},
+                            {
+                                // sort: { name: 1 },
+                                limit: [!!limit ? limit : 100],
+                                skip: [skip],
+                            },
+                        ]),
+                    }
+                )
             )
         );
     };

+ 18 - 16
src/actions/actionCatById.js

@@ -1,23 +1,25 @@
-import { backendURL, mock, query } from '../helpers';
+import { backendURL, mock, query, gql } from '../helpers';
 
 import { actionPromise } from '../reducers';
 
 export const actionCatById = ({ _id, promiseName = 'catById', orderBy = '', limit = 20, skip = 0 }) =>
     actionPromise(
         promiseName,
-        fetch(`${backendURL}/categories/${_id}/?limit=${limit}&skip=${skip}${orderBy && `&orderBy=` + orderBy}`, {
-            method: 'GET',
-            headers: {
-                'Content-Type': 'application/json',
-                Accept: 'application/json',
-
-                ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
-            },
-        })
-            .then((res) => res.json())
-            .then((data) => {
-                if (data.errors) {
-                    throw new Error(JSON.stringify(data.errors));
-                } else return data.data;
-            })
+        gql(
+            `query CatAll($q:String){
+                CategoryFindOne(query: $q){
+                    _id name
+                    parent{
+                        _id, name
+                    }
+                    subcategories{
+                        _id name
+                    }
+                    goods{
+                        _id name price amount
+                    }
+                }
+            }`,
+            { q: JSON.stringify([{ _id }]) }
+        )
     );

+ 23 - 0
src/actions/actionCategoryDelete.js

@@ -0,0 +1,23 @@
+import { backendURL, getQuery, mock, query } from '../helpers';
+
+import { actionPromise } from '../reducers';
+
+export const actionCategoryDelete = ({ _id, promiseName = 'categoryDelete' } = {}) =>
+    actionPromise(
+        promiseName,
+        fetch(`${backendURL}/category/${_id}/delete/`, {
+            method: 'GET',
+            headers: {
+                'Content-Type': 'application/json',
+                Accept: 'application/json',
+
+                ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+            },
+        })
+            .then((res) => res.json())
+            .then((data) => {
+                if (data.errors) {
+                    throw new Error(JSON.stringify(data.errors));
+                } else return data.data;
+            })
+    );

+ 1 - 1
src/actions/actionCategoryUpdate.js

@@ -3,5 +3,5 @@ import { actionCategoryUpsert } from './actionCategoryUpsert';
 
 export const actionCategoryUpdate = (good) => async (dispatch, getState) => {
     await dispatch(actionCategoryUpsert(good));
-    await dispatch(actionCatAll());
+    await setTimeout(() => dispatch(actionCatAll()), 1000);
 };

+ 4 - 4
src/actions/actionCategoryUpsert.js

@@ -4,10 +4,10 @@ import { actionPromise } from '../reducers';
 export const actionCategoryUpsert = (category) => async (dispatch) => {
     const formData = new FormData();
     category._id && formData.append('_id', category._id);
-    formData.append('name', category.name);
-    formData.append('goods', JSON.stringify(category.goods));
-    category.parent && formData.append('parent', JSON.stringify(category.parent));
-    formData.append('subcategories', JSON.stringify(category.subcategories));
+    category.name && formData.append('name', category.name);
+    category.goods && formData.append('goods', JSON.stringify(category.goods));
+    category.parent !== undefined && formData.append('parent', JSON.stringify(category.parent));
+    category.subcategories && formData.append('subcategories', JSON.stringify(category.subcategories));
     dispatch(
         actionPromise(
             'categoryUpsert',

+ 16 - 16
src/actions/actionGoodById.js

@@ -1,23 +1,23 @@
-import { backendURL, getQuery, mock, query } from '../helpers';
+import { backendURL, getQuery, gql, mock, query } from '../helpers';
 
 import { actionPromise } from '../reducers';
 
 export const actionGoodById = ({ _id, promiseName = 'goodById' } = {}) =>
     actionPromise(
         promiseName,
-        fetch(`${backendURL}/goods/${_id}/`, {
-            method: 'GET',
-            headers: {
-                'Content-Type': 'application/json',
-                Accept: 'application/json',
-
-                ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
-            },
-        })
-            .then((res) => res.json())
-            .then((data) => {
-                if (data.errors) {
-                    throw new Error(JSON.stringify(data.errors));
-                } else return data.data;
-            })
+        gql(
+            `query GoodById($q:String){
+            GoodFindOne(query: $q){
+                _id name amount description price 
+                categories{
+                    _id name
+                }
+                images{
+                    _id
+                    url
+                }
+            }
+        }`,
+            { q: JSON.stringify([{ _id }]) }
+        )
     );

+ 11 - 24
src/actions/actionGoodUpsert.js

@@ -1,32 +1,19 @@
-import { backendURL } from '../helpers';
+import { backendURL, gql } from '../helpers';
 import { actionPromise } from '../reducers';
 
 export const actionGoodUpsert = (good) => async (dispatch) => {
-    const formData = new FormData();
-    good._id && formData.append('_id', good._id);
-    formData.append('name', good.name);
-    formData.append('description', good.description);
-    formData.append('amount', good.amount);
-    formData.append('price', good.price);
-    formData.append('images', JSON.stringify(good.images));
-    formData.append('categories', JSON.stringify(good.categories));
+
     dispatch(
         actionPromise(
-            'goodUpsert',
-            fetch(`${backendURL}/good/`, {
-                method: 'POST',
-                headers: {
-                    accept: 'application/json',
-                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
-                },
-                body: formData,
-            })
-                .then((res) => res.json())
-                .then((data) => {
-                    if (data.errors) {
-                        throw new Error(JSON.stringify(data.errors));
-                    } else return data.data;
-                })
+            "goodUpsert",
+            gql(
+                `mutation GoodUpsert($good:GoodInput!){
+                    GoodUpsert(good:$good){
+                        _id name
+                    }
+                  }`,
+                { good }
+            )
         )
     );
 };

+ 23 - 15
src/actions/actionGoodsAll.js

@@ -1,4 +1,4 @@
-import { backendURL, getQuery } from '../helpers';
+import { backendURL, getQuery, gql } from '../helpers';
 import { actionPromise } from '../reducers';
 
 export const actionGoodsAll =
@@ -7,20 +7,28 @@ export const actionGoodsAll =
         dispatch(
             actionPromise(
                 promiseName,
-                fetch(`${backendURL}/goods/?limit=${limit}&skip=${skip}${orderBy && `&orderBy=` + orderBy}`, {
-                    method: 'GET',
-                    headers: {
-                        'Content-Type': 'application/json',
-                        accept: 'application/json',
-                        ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
-                    },
-                })
-                    .then((res) => res.json())
-                    .then((data) => {
-                        if (data.errors) {
-                            throw new Error(JSON.stringify(data.errors));
-                        } else return data.data;
-                    })
+                gql(
+                    `query GoodsAll($query:String){
+                        GoodFind(query: $query){
+                            _id name price images{
+                                _id url
+                            }
+                            categories{
+                                _id name
+                            }
+                            amount
+                        }
+                    }`,
+                    {
+                        query: JSON.stringify([
+                            {},
+                            {
+                                limit: !!limit ? limit : 100,
+                                skip: skip,
+                            },
+                        ]),
+                    }
+                )
             )
         );
     };

+ 19 - 15
src/actions/actionGoodsPopular.js

@@ -1,4 +1,4 @@
-import { backendURL, mock, query } from '../helpers';
+import { backendURL, mock, query, gql } from '../helpers';
 
 import { actionPromise } from '../reducers';
 
@@ -6,20 +6,24 @@ export const actionGoodsPopular = () => async (dispatch, getState) => {
     dispatch(
         actionPromise(
             'goodsPopular',
-            fetch(`${backendURL}/goods/?limit=20&skip=0&popular=1`, {
-                method: 'GET',
-                headers: {
-                    accept: 'application/json',
-                    'Content-Type': 'application/json',
-                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
-                },
-            })
-                .then((res) => res.json())
-                .then((data) => {
-                    if (data.errors) {
-                        throw new Error(JSON.stringify(data.errors));
-                    } else return data.data;
-                })
+            gql(
+                `query GoodsPopular($query:String){
+                    GoodFind(query: $query){
+                        _id name price amount
+                        images{
+                            _id url
+                        }
+                    }
+                }`,
+                {
+                    query: JSON.stringify([
+                        {},
+                        {
+                            sort: 'popular',
+                        },
+                    ]),
+                }
+            )
         )
     );
 };

+ 14 - 21
src/actions/actionLogin.js

@@ -2,30 +2,23 @@ import { actionPromise } from '../reducers';
 import { backendURL, gql } from '../helpers';
 import { actionAuthLogin } from '../reducers';
 
-export const actionLogin = (login, password) => async (dispatch, getState) => {
-    const formData = new FormData();
-    formData.append('username', login);
-    formData.append('password', password);
-
+export const actionLogin = (username, password) => async (dispatch, getState) => {
     const token = await dispatch(
         actionPromise(
             'login',
-            fetch(`${backendURL}/auth/token/`, {
-                method: 'POST',
-                headers: {
-                    accept: 'application/json',
-                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
-                },
-                body: formData,
-            })
-                .then((res) => res.json())
-                .then((data) => {
-                    if (data.errors) {
-                        throw new Error(JSON.stringify(data.errors));
-                    } else return data.access;
-                })
+            gql(
+                `mutation Login($username:String!,$password:String!){
+                    tokenAuth(username:$username,password:$password){
+                        token
+                    }
+                }`,
+                { username, password }
+            )
         )
     );
-
-    dispatch(actionAuthLogin(token));
+    if (typeof token === 'string') {
+        dispatch(actionAuthLogin(token));
+    } else {
+        dispatch(actionAuthLogin(token.token));
+    }
 };

+ 23 - 0
src/actions/actionOrderDelete.js

@@ -0,0 +1,23 @@
+import { backendURL, getQuery, mock, query } from '../helpers';
+
+import { actionPromise } from '../reducers';
+
+export const actionOrderDelete = ({ _id, promiseName = 'orderDelete' } = {}) =>
+    actionPromise(
+        promiseName,
+        fetch(`${backendURL}/order/${_id}/delete/`, {
+            method: 'GET',
+            headers: {
+                'Content-Type': 'application/json',
+                Accept: 'application/json',
+
+                ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+            },
+        })
+            .then((res) => res.json())
+            .then((data) => {
+                if (data.errors) {
+                    throw new Error(JSON.stringify(data.errors));
+                } else return data.data;
+            })
+    );

+ 15 - 15
src/actions/actionRootCats.js

@@ -1,4 +1,4 @@
-import { backendURL, mock, query } from '../helpers';
+import { backendURL, mock, query, gql } from '../helpers';
 
 import { actionPromise } from '../reducers';
 
@@ -6,20 +6,20 @@ export const actionRootCats = () => async (dispatch, getState) => {
     dispatch(
         actionPromise(
             'rootCats',
-            fetch(`${backendURL}/categories/?isRoot=1`, {
-                method: 'GET',
-                headers: {
-                    accept: 'application/json',
-                    'Content-Type': 'application/json',
-                    ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
-                },
-            })
-                .then((res) => res.json())
-                .then((data) => {
-                    if (data.errors) {
-                        throw new Error(JSON.stringify(data.errors));
-                    } else return data.data;
-                })
+            gql(
+                `query rootCats($query:String) {
+                CategoryFind(query: $query){
+                    _id name
+                }
+            }`,
+                {
+                    query: JSON.stringify([
+                        {
+                            parent: null,
+                        },
+                    ]),
+                }
+            )
         )
     );
 };

+ 15 - 6
src/components/AuthPage/AuthForm.js

@@ -14,7 +14,7 @@ const signInSchema = Yup.object().shape({
     password: Yup.string().required("Обов'язкове"),
 });
 
-export const AuthForm = ({ onSubmit = null, promiseStatus, serverErrors = [] } = {}) => {
+export const AuthForm = ({ onSubmit = null, promiseStatus, promisePayload, serverErrors = [] } = {}) => {
     const [showPassword, setShowPassword] = useState(false);
     const { setAlert } = useContext(UIContext);
     const navigate = useNavigate();
@@ -39,11 +39,19 @@ export const AuthForm = ({ onSubmit = null, promiseStatus, serverErrors = [] } =
     useEffect(() => {
         if (promiseStatus === 'FULFILLED') {
             formik.setSubmitting(false);
-            setAlert({
-                show: true,
-                severity: 'success',
-                message: 'Готово',
-            });
+            if (promisePayload) {
+                setAlert({
+                    show: true,
+                    severity: 'success',
+                    message: 'Готово',
+                });
+            } else {
+                setAlert({
+                    show: true,
+                    severity: 'error',
+                    message: 'Не вірні дані',
+                });
+            }
         }
         if (promiseStatus === 'REJECTED') {
             const errorMessage = serverErrors.reduce((prev, curr) => prev + '\n' + curr.message, '');
@@ -117,6 +125,7 @@ export const AuthForm = ({ onSubmit = null, promiseStatus, serverErrors = [] } =
 export const CAuthForm = connect(
     (state) => ({
         promiseStatus: state.promise?.login?.status || null,
+        promisePayload: state.promise?.login?.payload || null,
         serverErrors: state.promise?.login?.error || [],
     }),
     {

+ 4 - 0
src/components/GoodsPage/index.js

@@ -7,9 +7,13 @@ import { GoodList } from '../common/GoodList';
 import { SubCategories } from './SubCategories';
 import { SortOptions } from '../common/SortOptions';
 import { actionCatById } from '../../actions/actionCatById';
+import { useEffect } from 'react';
 
 const GoodsPage = ({ category = {} }) => {
     const { goods = [], name = '', subcategories = [] } = category || {};
+    useEffect(() => {
+        console.log(category);
+    }, [category]);
     const dispatch = useDispatch();
     return (
         <Box className="GoodsPage">

+ 2 - 2
src/components/LayoutPage/index.js

@@ -54,12 +54,12 @@ export const LayoutPage = () => {
         <Box className="LayoutPage">
             <Header />
             <Grid container columns={14} rows={1}>
-                {!!location.pathname.match(/(\/categor)|(\/good)|(\/order)+/) && (
+                {!!location.pathname.match(/(\/categor)|(\/good)|(\/order)|(\/admin)+/) && (
                     <Grid xs={3} item>
                         <Aside />
                     </Grid>
                 )}
-                <Grid xs={location.pathname.match(/(\/categor)|(\/good)|(\/order)+/) ? 11 : 14} item>
+                <Grid xs={location.pathname.match(/(\/categor)|(\/good)|(\/order)|(\/admin)+/) ? 11 : 14} item>
                     <Content>
                         <Routes>
                             <Route path="/" exact element={<MainPage />} />

+ 57 - 25
src/components/admin/AdminCategoryPage/CategoryForm.js

@@ -1,4 +1,4 @@
-import { connect } from 'react-redux';
+import { connect, useDispatch } from 'react-redux';
 import React, { useState, useEffect, useContext } from 'react';
 import Select from 'react-select';
 import { actionCategoryUpdate } from '../../../actions/actionCategoryUpdate';
@@ -8,6 +8,9 @@ import { UIContext } from '../../UIContext';
 import { useFormik } from 'formik';
 import * as Yup from 'yup';
 import { Error } from '../../common/Error';
+import { ConfirmModal } from '../../common/ConfirmModal';
+import { useNavigate } from 'react-router-dom';
+import { actionCategoryDelete } from '../../../actions/actionCategoryDelete';
 
 const categorySchema = Yup.object().shape({
     name: Yup.string().required("Обов'язкове"),
@@ -18,7 +21,9 @@ const CategoryForm = ({
     onSaveClick,
     onSave,
     onClose,
+    onDelete,
     promiseStatus,
+    deletePromiseStatus,
     catList: initialCatList = [],
     goodList = [],
     category = {},
@@ -29,7 +34,9 @@ const CategoryForm = ({
     const [subCatList, setSubCatList] = useState([]);
     const [parentList, setParentList] = useState([]);
     const { setAlert } = useContext(UIContext);
-
+    const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+    const navigate = useNavigate();
+    const dispatch = useDispatch();
     const formik = useFormik({
         initialValues: {
             name: category?.name || '',
@@ -76,6 +83,22 @@ const CategoryForm = ({
         }
     }, [promiseStatus]);
 
+    useEffect(() => {
+        if (deletePromiseStatus === 'FULFILLED') {
+            navigate('/admin/categories/');
+        }
+        if (deletePromiseStatus === 'REJECTED') {
+            setAlert({
+                show: true,
+                severity: 'error',
+                message: 'Помилка',
+            });
+        }
+        return () => {
+            dispatch(actionPromiseClear('categoryDelete'));
+        };
+    }, [deletePromiseStatus]);
+
     useEffect(() => {
         let parentList = initialCatList.filter(
             ({ _id }) =>
@@ -103,10 +126,6 @@ const CategoryForm = ({
 
     return (
         <Box className="CategoryForm" component="form" onSubmit={formik.handleSubmit}>
-            {(serverErrors || []).map((error) => (
-                <Error>{error?.message}</Error>
-            ))}
-
             <Box>
                 <TextField
                     id="name"
@@ -143,40 +162,53 @@ const CategoryForm = ({
                     isMulti={true}
                 />
             </Box>
-            {
-                <Box sx={{ mt: 3 }}>
-                    <InputLabel className="form-label">Товари</InputLabel>
-                    <Select
-                        value={inputGoods?.map(({ _id, name }) => ({ value: _id, label: name }))}
-                        closeMenuOnSelect={false}
-                        onChange={(e) => setInputGoods(e.map(({ value, label }) => ({ _id: value, name: label })))}
-                        options={goodList?.map(({ _id, name }) => ({ value: _id, label: name }))}
-                        isMulti={true}
-                    />
-                </Box>
-            }
-            <Box direction="row" sx={{ mt: 3 }} justifyContent="flex-end">
-                <Button variant="contained" disabled={!formik.isValid || formik.isSubmitting} type="submit" fullWidth>
+
+            <Box sx={{ mt: 3 }}>
+                <InputLabel className="form-label">Товари</InputLabel>
+                <Select
+                    value={inputGoods?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    closeMenuOnSelect={false}
+                    onChange={(e) => setInputGoods(e.map(({ value, label }) => ({ _id: value, name: label })))}
+                    options={goodList?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    isMulti={true}
+                />
+            </Box>
+            <Stack direction="row" sx={{ mt: 3 }} justifyContent="flex-end" spacing={1}>
+                {!!category._id && (
+                    <Button variant="contained" onClick={() => setIsDeleteModalOpen(true)} color="error">
+                        Видалити
+                    </Button>
+                )}
+                <Button variant="contained" disabled={!formik.isValid || formik.isSubmitting} type="submit">
                     Зберегти
                 </Button>
-            </Box>
+            </Stack>
+            {!!category._id && (
+                <ConfirmModal
+                    open={isDeleteModalOpen}
+                    text="Видалити категорію?"
+                    onClose={() => setIsDeleteModalOpen(false)}
+                    onNO={() => setIsDeleteModalOpen(false)}
+                    onYES={() => {
+                        onDelete(category._id);
+                    }}
+                />
+            )}
         </Box>
     );
 };
 
-// const CRegisterForm = connect((state) => ({ serverErrors: state.promise?.register?.error || [] }), {
-//     onRegister: (login, password) => actionRegister(login, password),
-// })(RegisterForm);
-
 export const CCategoryForm = connect(
     (state) => ({
         catList: state.promise.catAll?.payload || [],
         promiseStatus: state.promise.categoryUpsert?.status || null,
         serverErrors: state.promise.categoryUpsert?.error || null,
         goodList: state.promise.goodsAll?.payload || [],
+        deletePromiseStatus: state.promise.categoryDelete?.status || null,
     }),
     {
         onSave: (cat) => actionCategoryUpdate(cat),
         onClose: () => actionPromiseClear('categoryUpsert'),
+        onDelete: (_id) => actionCategoryDelete({ _id }),
     }
 )(CategoryForm);

+ 113 - 0
src/components/admin/AdminCategoryTree/index.js

@@ -0,0 +1,113 @@
+import 'react-sortable-tree/style.css';
+import { Component, useState, useEffect } from 'react';
+import { connect } from 'react-redux';
+import SortableTree from 'react-sortable-tree';
+import { FaEdit } from 'react-icons/fa';
+import { actionCategoryUpsert } from '../../../actions/actionCategoryUpsert';
+import { CategoryEditModal } from '../CategoryEditModal';
+import { Box, Button } from '@mui/material';
+
+const bulidCategoryTree = (list) => {
+    let node,
+        roots = [],
+        map = {},
+        newList = [];
+
+    for (let i = 0; i < list.length; i += 1) {
+        newList[i] = {};
+        newList[i].children = [];
+        newList[i].title = list[i].name;
+        newList[i].parent = list[i].parent;
+        newList[i]._id = list[i]._id;
+        map[newList[i]._id] = i;
+    }
+
+    for (let i = 0; i < list.length; i += 1) {
+        node = newList[i];
+        if (list[i].subcategories) {
+            for (let subCat of list[i].subcategories) {
+                node.children.push(newList[map[subCat._id]]);
+            }
+        }
+    }
+
+    for (let i = 0; i < list.length; i += 1) {
+        node = newList[i];
+        if (node.parent === null) {
+            roots.push(node);
+        }
+    }
+    return roots;
+};
+
+const bulidCategoryList = (tree) => {
+    let list = [];
+    for (let node of tree) {
+        list = [...list, node];
+        if (!!node.children?.length) {
+            list = [...list, ...bulidCategoryList(node.children)];
+        }
+    }
+    return list;
+};
+
+export const AdminCategoryTree = ({ categories, onDrop, onPopupOpen }) => {
+    const [treeData, setTreeData] = useState([]);
+    const [selectedNode, setSelectedNode] = useState(null);
+    const [isCategoryPopupOpen, setIsCategoryPopupOpen] = useState(false);
+
+    useEffect(() => {
+        setTreeData(bulidCategoryTree(categories));
+    }, [categories]);
+
+    return (
+        <Box className="CategotyTree">
+            <CategoryEditModal
+                category={selectedNode}
+                isOpen={isCategoryPopupOpen}
+                onClose={() => setIsCategoryPopupOpen(false)}
+            />
+            <SortableTree
+                isVirtualized={false}
+                treeData={treeData}
+                onChange={(treeData) => setTreeData(treeData)}
+                generateNodeProps={({ node, parentNode }) => ({
+                    onDrop: () => {
+                        const { _id, title: name } = node;
+                        if (parentNode) {
+                            let { _id, title: name } = parentNode;
+                            let subcategories =
+                                parentNode?.children?.map(({ _id, title: name }) => ({ _id, name })) || [];
+                            onDrop({ _id, name, subcategories });
+                        } else {
+                            onDrop({ _id, name, parent: null });
+                        }
+                    },
+                    buttons: [
+                        <Button
+                            className="editButton"
+                            onClick={() => {
+                                let parent = null;
+                                let { title: name, children: subcategories, _id } = { ...node };
+                                subcategories = subcategories.map(({ _id, title: name }) => ({ _id, name }));
+                                if (parentNode) {
+                                    let { title: name, _id } = parentNode;
+                                    parent = { _id, name };
+                                }
+                                setSelectedNode({ name, _id, subcategories, parent });
+                                setIsCategoryPopupOpen(true);
+                            }}
+                        >
+                            <FaEdit />
+                        </Button>,
+                    ],
+                    className: 'TreeNode',
+                })}
+            />
+        </Box>
+    );
+};
+
+export const CAdminCategoryTree = connect((state) => ({ categories: state.promise?.catAll?.payload || [] }), {
+    onDrop: (category) => actionCategoryUpsert(category),
+})(AdminCategoryTree);

+ 1 - 1
src/components/admin/AdminGoodPage/GoodForm.js

@@ -58,8 +58,8 @@ export const GoodForm = ({
 } = {}) => {
     const [inputCategories, setInputCategories] = useState([]);
     const [inputImages, setInputImages] = useState([]);
-    const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
     const { setAlert } = useContext(UIContext);
+    const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
     const navigate = useNavigate();
     const dispatch = useDispatch();
     const formik = useFormik({

+ 3 - 1
src/components/admin/AdminLayoutPage/index.js

@@ -16,6 +16,7 @@ import { CAdminOrderPage } from '../AdminOrderPage';
 import { actionOrderById } from '../../../actions/actionOrderById';
 import { actionCatAll } from '../../../actions/actionCatAll';
 import { actionGoodsAll } from '../../../actions/actionGoodsAll';
+import { CAdminCategoryTree } from '../AdminCategoryTree';
 
 const AdminCategoryPageContainer = ({}) => {
     const dispatch = useDispatch();
@@ -197,7 +198,8 @@ const AdminLayoutPage = () => {
     return (
         <Box className="AdminLayoutPage">
             <Routes>
-                <Route path="/" element={<Navigate to={'/admin/goods/'} />} exact />
+                <Route path="/" element={<Navigate to={'/admin/goods/'} />} />
+                <Route path="/tree/" element={<CAdminCategoryTree />} />
                 <Route path="/goods/" element={<CAdminGoodsPageContainer />} />
                 <Route path="/good/" element={<AdminGoodPageContainer />} />
                 <Route path="/good/:_id" element={<AdminGoodPageContainer />} />

+ 56 - 14
src/components/admin/AdminOrderPage/OrderForm.js

@@ -1,4 +1,4 @@
-import { connect, useSelector } from 'react-redux';
+import { connect, useDispatch, useSelector } from 'react-redux';
 import React, { useState, useEffect, useContext } from 'react';
 import { actionPromise, actionPromiseClear } from '../../../reducers';
 import Select from 'react-select';
@@ -25,6 +25,9 @@ import * as Yup from 'yup';
 import { Error } from '../../common/Error';
 import { statusNumber, statusOptions } from '../../../helpers';
 import { OrderGoodsEditor } from './OrderGoodsEditor';
+import { useNavigate } from 'react-router-dom';
+import { actionOrderDelete } from '../../../actions/actionOrderDelete';
+import { ConfirmModal } from '../../common/ConfirmModal';
 
 const deliveryOptions = [
     { label: 'Нова пошта', value: 'nova-poshta' },
@@ -45,11 +48,23 @@ const orderSchema = Yup.object().shape({
         ),
 });
 
-export const OrderForm = ({ serverErrors = [], onSaveClick, onSave, onClose, promiseStatus, order = {} } = {}) => {
+export const OrderForm = ({
+    serverErrors = [],
+    onSaveClick,
+    onSave,
+    onClose,
+    onDelete,
+    promiseStatus,
+    deletePromiseStatus,
+    order = {},
+} = {}) => {
     const [inputStatus, setInputStatus] = useState(null);
     const { setAlert } = useContext(UIContext);
     const goodList = useSelector((state) => state.promise?.goodsAll?.payload || []);
     const [inputOrderGoods, setInputOrderGoods] = useState([]);
+    const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
+    const navigate = useNavigate();
+    const dispatch = useDispatch();
 
     const formik = useFormik({
         initialValues: {
@@ -113,16 +128,30 @@ export const OrderForm = ({ serverErrors = [], onSaveClick, onSave, onClose, pro
         }
     }, [promiseStatus]);
 
+    useEffect(() => {
+        if (deletePromiseStatus === 'FULFILLED') {
+            navigate('/admin/orders/');
+        }
+        if (deletePromiseStatus === 'REJECTED') {
+            setAlert({
+                show: true,
+                severity: 'error',
+                message: 'Помилка',
+            });
+        }
+        return () => {
+            dispatch(actionPromiseClear('orderDelete'));
+        };
+    }, [deletePromiseStatus]);
+
     useEffect(() => {
         return () => {
             onClose && onClose();
         };
     }, []);
+
     return (
         <Box className="OrderForm" component="form" onSubmit={formik.handleSubmit}>
-            {(serverErrors || []).map((error) => (
-                <Error>{error?.message}</Error>
-            ))}
             <Grid container spacing={5}>
                 <Grid item xs={6}>
                     <TextField
@@ -176,17 +205,16 @@ export const OrderForm = ({ serverErrors = [], onSaveClick, onSave, onClose, pro
                             }}
                         />
                     </Box>
-
-                    <Box direction="row" sx={{ mt: 3 }} justifyContent="flex-end">
-                        <Button
-                            variant="contained"
-                            disabled={Boolean(!formik.isValid || formik.isSubmitting)}
-                            type="submit"
-                            fullWidth
-                        >
+                    <Stack direction="row" sx={{ mt: 3 }} justifyContent="flex-end" spacing={1}>
+                        {!!order._id && (
+                            <Button variant="contained" onClick={() => setIsDeleteModalOpen(true)} color="error">
+                                Видалити
+                            </Button>
+                        )}
+                        <Button variant="contained" disabled={!formik.isValid || formik.isSubmitting} type="submit">
                             Зберегти
                         </Button>
-                    </Box>
+                    </Stack>
                 </Grid>
                 <Grid item xs={6}>
                     <TextField
@@ -255,6 +283,18 @@ export const OrderForm = ({ serverErrors = [], onSaveClick, onSave, onClose, pro
                     </TextField>
                 </Grid>
             </Grid>
+
+            {!!order._id && (
+                <ConfirmModal
+                    open={isDeleteModalOpen}
+                    text="Видалити замовлення?"
+                    onClose={() => setIsDeleteModalOpen(false)}
+                    onNO={() => setIsDeleteModalOpen(false)}
+                    onYES={() => {
+                        onDelete(order._id);
+                    }}
+                />
+            )}
         </Box>
     );
 };
@@ -264,9 +304,11 @@ export const COrderForm = connect(
         promiseStatus: state.promise.orderUpsert?.status || null,
         serverErrors: state.promise.orderUpsert?.error || null,
         order: state.promise?.adminOrderById?.payload || {},
+        deletePromiseStatus: state.promise.orderDelete?.status || null,
     }),
     {
         onSave: (order) => actionOrderUpdate(order),
         onClose: () => actionPromiseClear('orderUpsert'),
+        onDelete: (_id) => actionOrderDelete({ _id }),
     }
 )(OrderForm);

+ 18 - 0
src/components/admin/CategoryEditModal/index.js

@@ -0,0 +1,18 @@
+import { useEffect } from 'react';
+import { Modal } from '../../common/Modal';
+import { CCategoryForm } from '../AdminCategoryPage/CategoryForm';
+
+export const CategoryEditModal = ({ isOpen = false, onClose, category, onOpen } = {}) => {
+    useEffect(() => {
+        if (isOpen) {
+            onOpen();
+        }
+    }, isOpen);
+    return (
+        <div className="NodeEditModal">
+            <Modal open={isOpen} onClose={() => onClose()}>
+                <CCategoryForm category={category} />
+            </Modal>
+        </div>
+    );
+};

+ 1 - 1
src/components/common/ConfirmModal/index.js

@@ -8,7 +8,7 @@ export const ConfirmModal = ({ open, text, onYES, onNO, onClose }) => {
                 <Typography textAlign="center" variant="h5">
                     {text}
                 </Typography>
-                <Stack direction="row" justifyContent="space-between">
+                <Stack direction="row" justifyContent="space-between" mt={4}>
                     <Button variant="contained" onClick={() => onNO && onNO()} color="error">
                         Ні
                     </Button>

+ 1 - 1
src/components/common/EntityEditor/index.js

@@ -57,7 +57,7 @@ export const EntityEditor = ({ entity = { images: [] }, onSave, onFileDrop, uplo
                                         <Box
                                             component="img"
                                             className="DropZoneImage"
-                                            src={`${image.url}`}
+                                            src={`/${image.url}`}
                                             loading="lazy"
                                         />
                                     </ImageListItem>

+ 4 - 0
src/components/common/GoodList/index.js

@@ -1,7 +1,11 @@
 import { Box, Grid } from '@mui/material';
+import { useEffect } from 'react';
 import { GoodCard } from '../GoodCard';
 
 export const GoodList = ({ goods = [] } = {}) => {
+    useEffect(() => {
+        console.log(goods);
+    }, [goods]);
     return (
         <Box className="GoodList">
             <Grid container spacing={2}>

+ 4 - 0
src/components/layout/Aside/AdminCategories.js

@@ -13,6 +13,10 @@ const adminCategories = [
         _id: 'orders/',
         name: 'Замовлення',
     },
+    {
+        _id: 'tree/',
+        name: 'Дерево категорій',
+    },
 ];
 
 export const AdminCategories = () => <Categories categories={adminCategories} url="/admin/" />;

+ 15 - 0
src/helpers/GraphQL.js

@@ -0,0 +1,15 @@
+export const getGQL = (url) => (query, variables) =>
+    fetch(url, {
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+            ...(localStorage.authToken ? { Authorization: 'Bearer ' + localStorage.authToken } : {}),
+        },
+        body: JSON.stringify({ query, variables }),
+    })
+        .then((res) => res.json())
+        .then((data) => {
+            if (data.errors) {
+                throw new Error(JSON.stringify(data.errors));
+            } else return Object.values(data.data)[0];
+        });

+ 2 - 0
src/helpers/index.js

@@ -1,7 +1,9 @@
 import { jwtDecode } from './jwtDecode';
 import { mock } from './mock';
+import { getGQL } from './GraphQL';
 import { delay } from './delay';
 import { statusNumber, statusOptions } from './orderStatus';
 
 export const backendURL = '';
+export const gql = getGQL(backendURL + '/graphql/');
 export { jwtDecode, mock, delay, statusNumber, statusOptions };

+ 0 - 6
src/index.scss

@@ -33,12 +33,6 @@
 }
 
 
-.ConfirmModal{
-
-  & .modalContent{
-    max-width: 400px!important;
-  }
-}
 
 
 

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 10546 - 0
yarn.lock