Browse Source

+Admin Categories

ilya_shyian 2 years ago
parent
commit
f181331b45

+ 2 - 2
src/actions/actionCatAll.js

@@ -45,14 +45,14 @@ export const actionCatAll =
                                           parent: 1,
                                           subcategories: [],
                                           goods: [],
-                                          name: 'Category 4',
+                                          name: 'Category 5',
                                       },
                                       {
                                           _id: 6,
                                           parent: 1,
                                           subcategories: [],
                                           goods: [],
-                                          name: 'Category 4',
+                                          name: 'Category 6',
                                       },
                                   ],
                               })

+ 7 - 0
src/actions/actionCategoryUpdate.js

@@ -0,0 +1,7 @@
+import { actionCatAll } from './actionCatAll';
+import { actionCategoryUpsert } from './actionCategoryUpsert';
+
+export const actionCategoryUpdate = (good) => async (dispatch, getState) => {
+    await dispatch(actionCategoryUpsert(good));
+    await dispatch(actionCatAll());
+};

+ 27 - 0
src/actions/actionCategoryUpsert.js

@@ -0,0 +1,27 @@
+import { actionPromise } from '../reducers';
+
+export const actionCategoryUpsert = (good) => async (dispatch) => {
+    dispatch(
+        actionPromise(
+            'categoryUpsert',
+            new Promise((resolve) => {
+                setTimeout(
+                    Math.random() > 0.01
+                        ? resolve({
+                              data: {
+                                  _id: 6,
+                                  parent: 1,
+                                  subcategories: [],
+                                  goods: [],
+                                  name: 'Category 4',
+                              },
+                          })
+                        : resolve({
+                              errors: [{ message: 'Error adsasdadas' }],
+                          }),
+                    400
+                );
+            })
+        )
+    );
+};

+ 17 - 0
src/components/admin/AdminCategoriesPage/AdminCategoryItem.js

@@ -0,0 +1,17 @@
+import { Button, TableCell, TableRow } from '@mui/material';
+import { FaEdit } from 'react-icons/fa';
+import { Link } from 'react-router-dom';
+const AdminCategoryItem = ({ category }) => (
+    <TableRow className="AdminCategoryItem">
+        <TableCell scope="row">{category._id}</TableCell>
+        <TableCell>{category.name ? category.name : '-'}</TableCell>
+        <TableCell>{category.parent?.name ? category.parent.name : '-'}</TableCell>
+        <TableCell className="edit">
+            <Button component={Link} className="Link" to={`/admin/category/${category._id}/`}>
+                Редагувати
+            </Button>
+        </TableCell>
+    </TableRow>
+);
+
+export { AdminCategoryItem };

+ 42 - 0
src/components/admin/AdminCategoriesPage/AdminCategoryList.js

@@ -0,0 +1,42 @@
+import { AdminCategoryItem } from './AdminCategoryItem';
+import { AdminCategoryListHeader } from './AdminCategoryListHeader';
+import { connect } from 'react-redux';
+import { actionCatsFind } from '../../../actions/actionCatsFind';
+import { actionPromiseClear } from '../../../reducers';
+import { SearchBar, SearchResults } from '../../common/SearchBar';
+import { Box, Table, TableBody, TableHead } from '@mui/material';
+
+const CSearchBar = connect(null, {
+    onSearch: (text) => actionCatsFind({ promiseName: 'adminCatsFind', text, limit: 5 }),
+    onSearchButtonClick: () => actionPromiseClear('adminCatsFind'),
+})(SearchBar);
+
+const CSearchResults = connect((state) => ({ items: state.promise.adminCatsFind?.payload || [] }))(SearchResults);
+
+const AdminCategoryList = ({ categories }) => {
+    return (
+        <Box className="AdminCategoryList">
+            <Box className="searchBarWrapper">
+                <CSearchBar
+                    render={CSearchResults}
+                    searchLink="/admin/category/search/"
+                    renderParams={{ itemLink: '/admin/category/' }}
+                />
+            </Box>
+            <Table>
+                <TableHead>
+                    <AdminCategoryListHeader />
+                </TableHead>
+                <TableBody>
+                    {(categories || []).map((cat) => (
+                        <AdminCategoryItem category={cat} key={cat._id} />
+                    ))}
+                </TableBody>
+            </Table>
+        </Box>
+    );
+};
+
+const CAdminCategoryList = connect((state) => ({ categories: state.feed?.payload || [] }))(AdminCategoryList);
+
+export { AdminCategoryList, CAdminCategoryList };

+ 25 - 0
src/components/admin/AdminCategoriesPage/AdminCategoryListHeader.js

@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import { AddButton } from '../../common/AddButton';
+
+import { TableCell, TableRow, TableSortLabel } from '@mui/material';
+import { useNavigate } from 'react-router-dom';
+
+const AdminCategoryListHeader = ({ sort, onSortChange, sortReversed, onSortReverseChange }) => {
+    const navigate = useNavigate();
+    return (
+        <TableRow className="AdminCategoryListHeader">
+            <TableCell scope="col">#</TableCell>
+            <TableCell scope="col">Название</TableCell>
+            <TableCell scope="col">Родительская категория</TableCell>
+            <TableCell scope="col">
+                <AddButton
+                    onClick={() => {
+                        navigate(`/admin/category/`);
+                    }}
+                />
+            </TableCell>
+        </TableRow>
+    );
+};
+
+export { AdminCategoryListHeader };

+ 13 - 0
src/components/admin/AdminCategoriesPage/index.js

@@ -0,0 +1,13 @@
+import { Box, Typography } from '@mui/material';
+import { CAdminCategoryList } from './AdminCategoryList';
+
+export const AdminCategoriesPage = () => {
+    return (
+        <Box className="AdminCategoriesPage">
+            <Typography variant="h5" sx={{ marginBottom: '10px', marginTop: '10px' }}>
+                Категорії
+            </Typography>
+            <CAdminCategoryList />
+        </Box>
+    );
+};

+ 161 - 0
src/components/admin/AdminCategoryPage/CategoryForm.js

@@ -0,0 +1,161 @@
+import { connect } from 'react-redux';
+import React, { useState, useEffect } from 'react';
+import Select from 'react-select';
+import { actionCategoryUpdate } from '../../../actions/actionCategoryUpdate';
+import { actionPromise, actionPromiseClear, store } from '../../../reducers';
+import { Box, Button, InputLabel, Stack, TextField, Typography } from '@mui/material';
+
+import { useFormik } from 'formik';
+import * as Yup from 'yup';
+import { Error } from '../../common/Error';
+
+const categorySchema = Yup.object().shape({
+    name: Yup.string().required("Обов'язкове"),
+});
+
+const CategoryForm = ({
+    serverErrors,
+    onSaveClick,
+    onSave,
+    onClose,
+    promiseStatus,
+    goodsField = false,
+    catList: initialCatList = [],
+    goodList = [],
+    category = {},
+} = {}) => {
+    const [inputSubCategories, setInputSubCategories] = useState([]);
+    const [inputGoods, setInputGoods] = useState([]);
+    const [inputParent, setInputParent] = useState({});
+    const [subCatList, setSubCatList] = useState([]);
+    const [parentList, setParentList] = useState([]);
+
+    const formik = useFormik({
+        initialValues: {
+            name: category?.name || '',
+        },
+        validationSchema: categorySchema,
+        validateOnChange: true,
+        onSubmit: () => {
+            let categoryToSave = {};
+            category?._id && (categoryToSave._id = category?._id);
+            categoryToSave.name = formik.values.name;
+            inputGoods && (categoryToSave.goods = inputGoods);
+            inputParent && (categoryToSave.parent = inputParent);
+
+            categoryToSave.subCategories = inputSubCategories;
+            onSaveClick && onSaveClick();
+            onSave(categoryToSave);
+        },
+    });
+
+    useEffect(() => {
+        setInputParent(category?.parent || null);
+        setInputGoods(category?.goods || []);
+        setInputSubCategories(category?.subCategories || []);
+    }, []);
+
+    useEffect(() => {
+        let parentList = initialCatList.filter(
+            ({ _id }) =>
+                !category?.subCatergories?.find((subCat) => _id === subCat._id) &&
+                _id !== category?._id &&
+                !inputSubCategories?.find((subCat) => _id === subCat._id)
+        );
+        parentList = [...[{ _id: null, name: 'null' }], ...parentList];
+
+        setParentList(parentList);
+    }, [inputSubCategories]);
+
+    useEffect(() => {
+        let subCatList = initialCatList.filter(
+            ({ _id }) => _id !== category?.parent?._id && _id !== category?._id && inputParent?._id !== _id
+        );
+        setSubCatList(subCatList);
+    }, [inputParent]);
+
+    useEffect(() => {
+        return () => {
+            onClose && onClose();
+        };
+    }, []);
+
+    return (
+        <Box className="CategoryForm" component="form" onSubmit={formik.handleSubmit}>
+            {(serverErrors || []).map((error) => (
+                <Error>{error?.message}</Error>
+            ))}
+
+            <Box>
+                <TextField
+                    id="name"
+                    name="name"
+                    variant="outlined"
+                    label="Назва"
+                    size="small"
+                    error={formik.touched.name && Boolean(formik.errors.name)}
+                    value={formik.values.name}
+                    onBlur={formik.handleBlur}
+                    onChange={formik.handleChange}
+                    helperText={formik.touched.name && formik.errors.name}
+                    multiline
+                    fullWidth
+                    sx={{ mt: 2 }}
+                />
+            </Box>
+            <Box sx={{ mt: 3 }}>
+                <InputLabel className="form-label">Батьківська категорія</InputLabel>
+                <Select
+                    value={{ value: inputParent?._id || null, label: inputParent?.name || 'null' }}
+                    onChange={(e) => setInputParent({ _id: e.value, name: e.label })}
+                    options={parentList.map(({ _id, name }) => ({ value: _id, label: name }))}
+                />
+            </Box>
+
+            <Box sx={{ mt: 3 }}>
+                <InputLabel className="form-label">Підкатегорії</InputLabel>
+                <Select
+                    value={inputSubCategories?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    closeMenuOnSelect={false}
+                    onChange={(e) => setInputSubCategories(e.map(({ value, label }) => ({ _id: value, name: label })))}
+                    options={subCatList?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    isMulti={true}
+                />
+            </Box>
+            {goodsField && (
+                <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>
+                    Зберегти
+                </Button>
+            </Box>
+        </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,
+        goodList: state.promise.goodsAll?.payload || [],
+    }),
+    {
+        onSave: (cat) => actionCategoryUpdate(cat),
+        onClose: () => actionPromiseClear('categoryUpsert'),
+    }
+)(CategoryForm);

+ 199 - 0
src/components/admin/AdminCategoryPage/GoodForm.js

@@ -0,0 +1,199 @@
+import { connect } from 'react-redux';
+import React, { useState, useEffect } from 'react';
+import { actionPromise, actionPromiseClear } from '../../../reducers';
+import Select from 'react-select';
+import { actionGoodUpdate } from '../../../actions/actionGoodUpdate';
+import { EntityEditor } from '../../common/EntityEditor';
+import { actionUploadFiles } from '../../../actions/actionUploadFiles';
+import {
+    Box,
+    Button,
+    Chip,
+    FormControl,
+    InputLabel,
+    MenuItem,
+    OutlinedInput,
+    Stack,
+    TextareaAutosize,
+    TextField,
+    Typography,
+} from '@mui/material';
+import { useFormik } from 'formik';
+import * as Yup from 'yup';
+import { Error } from '../../common/Error';
+
+const goodSchema = Yup.object().shape({
+    name: Yup.string().required("Обов'язкове"),
+    description: Yup.string().required("Обов'язкове"),
+    price: Yup.number().min(0, 'більше або равно 0').required("Обов'язкове"),
+    amount: Yup.number().min(0, 'більше або равно 0').required("Обов'язкове"),
+});
+
+const CGoodEditor = connect(
+    (state) => ({
+        entity: state.promise?.adminGoodById?.payload || {},
+        uploadFiles: state.promise?.uploadFiles,
+    }),
+    {
+        onFileDrop: (files) => actionUploadFiles(files),
+    }
+)(EntityEditor);
+
+export const GoodForm = ({
+    serverErrors,
+    onSaveClick,
+    onSave,
+    onClose,
+    promiseStatus,
+    catList = [],
+    good = {},
+} = {}) => {
+    const [inputCategories, setInputCategories] = useState([]);
+    const [inputImages, setInputImages] = useState([]);
+
+    const formik = useFormik({
+        initialValues: {
+            name: '',
+            description: '',
+            price: 0,
+            amount: 0,
+        },
+        validationSchema: goodSchema,
+        validateOnChange: true,
+        onSubmit: () => {
+            let goodToSave = {};
+            good?._id && (goodToSave._id = good._id);
+            goodToSave.name = formik.values.name;
+            goodToSave.description = formik.values.description;
+            goodToSave.price = +formik.values.price;
+            goodToSave.amount = +formik.values.amount;
+            goodToSave.categories = inputCategories;
+            goodToSave.images = inputImages?.map(({ _id }) => ({ _id })) || [];
+
+            onSaveClick && onSaveClick();
+            onSave(goodToSave);
+        },
+    });
+
+    useEffect(() => {
+        setInputCategories(good?.categories || []);
+        setInputImages(good?.images || []);
+        formik.setFieldValue('name', good.name || '');
+        formik.setFieldValue('description', good.description || '');
+        formik.setFieldValue('amount', good.amount || 0);
+        formik.setFieldValue('price', good.price || 0);
+    }, [good]);
+
+    useEffect(() => {
+        return () => {
+            onClose && onClose();
+        };
+    }, []);
+    return (
+        <Box className="GoodForm" component="form" onSubmit={formik.handleSubmit}>
+            {(serverErrors || []).map((error) => (
+                <Error>{error?.message}</Error>
+            ))}
+
+            <TextField
+                id="name"
+                name="name"
+                variant="outlined"
+                label="Назва"
+                size="small"
+                error={formik.touched.name && Boolean(formik.errors.name)}
+                value={formik.values.name}
+                onBlur={formik.handleBlur}
+                onChange={formik.handleChange}
+                helperText={formik.touched.name && formik.errors.name}
+                multiline
+                fullWidth
+                sx={{ mt: 2 }}
+            />
+
+            <Box sx={{ mt: 3 }}>
+                <InputLabel>Зображення</InputLabel>
+                <CGoodEditor onImagesSave={(images) => setInputImages(images)} />
+            </Box>
+
+            <TextField
+                variant="outlined"
+                id="description"
+                name="description"
+                label="Опис"
+                size="small"
+                error={formik.touched.description && Boolean(formik.errors.description)}
+                value={formik.values.description}
+                onBlur={formik.handleBlur}
+                onChange={formik.handleChange}
+                helperText={formik.touched.description && formik.errors.description}
+                multiline
+                fullWidth
+                sx={{ mt: 2 }}
+            />
+
+            <Box sx={{ mt: 3 }}>
+                <TextField
+                    variant="outlined"
+                    id="price"
+                    name="price"
+                    label="Ціна"
+                    size="small"
+                    error={formik.touched.price && Boolean(formik.errors.price)}
+                    value={formik.values.price}
+                    onBlur={formik.handleBlur}
+                    onChange={formik.handleChange}
+                    helperText={formik.touched.price && formik.errors.price}
+                    multiline
+                    fullWidth
+                    sx={{ mt: 2 }}
+                />
+            </Box>
+
+            <Box sx={{ mt: 3 }}>
+                <InputLabel>Категорії</InputLabel>
+                <Select
+                    placeholder="Обрати категорії"
+                    value={inputCategories.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    closeMenuOnSelect={false}
+                    onChange={(e) => setInputCategories(e.map(({ label, value }) => ({ _id: value, name: label })))}
+                    options={catList?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    isMulti={true}
+                />
+                {/* <TextField
+                        classes={{ root: classes.root }}
+                        select
+                        name="userRoles"
+                        id="userRoles"
+                        variant="outlined"
+                        label="userRoles"
+                        SelectProps={{
+                            multiple: true,
+                            value: formState.userRoles,
+                            onChange: catList?.map(({ _id, name }) => ({ value: _id, label: name })),
+                        }}
+                    >
+                        {catList?.map(({ _id, name }) => ({ value: _id, label: name }))}
+                    </TextField> */}
+            </Box>
+
+            <Box direction="row" sx={{ mt: 3 }} justifyContent="flex-end">
+                <Button variant="contained" disabled={!formik.isValid || formik.isSubmitting} type="submit" fullWidth>
+                    Зберегти
+                </Button>
+            </Box>
+        </Box>
+    );
+};
+
+export const CGoodForm = connect(
+    (state) => ({
+        catList: state.promise.catAll?.payload || [],
+        promiseStatus: state.promise.goodUpsert?.status || null,
+        good: state.promise?.adminGoodById?.payload || {},
+    }),
+    {
+        onSave: (good) => actionGoodUpdate(good),
+        onClose: () => actionPromiseClear('goodUpsert'),
+    }
+)(GoodForm);

+ 12 - 0
src/components/admin/AdminCategoryPage/index.js

@@ -0,0 +1,12 @@
+import { Box } from '@mui/material';
+import { connect } from 'react-redux';
+import { CCategoryForm } from './CategoryForm';
+
+export const AdminCategoryPage = ({ good }) => (
+    <Box className="AdminCategoryPage">
+        <CCategoryForm good={good} />
+    </Box>
+);
+export const CAdminCategoryPage = connect((state) => ({ good: state.promise?.adminCatById?.payload || {} }))(
+    AdminCategoryPage
+);

+ 5 - 2
src/components/admin/AdminGoodPage/GoodForm.js

@@ -25,8 +25,8 @@ import { Error } from '../../common/Error';
 const goodSchema = Yup.object().shape({
     name: Yup.string().required("Обов'язкове"),
     description: Yup.string().required("Обов'язкове"),
-    price: Yup.number().min(0, 'більше або равно 0'),
-    amount: Yup.number().min(0, 'більше або равно 0'),
+    price: Yup.number().min(0, 'більше або равно 0').required("Обов'язкове"),
+    amount: Yup.number().min(0, 'більше або равно 0').required("Обов'язкове"),
 });
 
 const CGoodEditor = connect(
@@ -100,6 +100,7 @@ export const GoodForm = ({
                 name="name"
                 variant="outlined"
                 label="Назва"
+                size="small"
                 error={formik.touched.name && Boolean(formik.errors.name)}
                 value={formik.values.name}
                 onBlur={formik.handleBlur}
@@ -120,6 +121,7 @@ export const GoodForm = ({
                 id="description"
                 name="description"
                 label="Опис"
+                size="small"
                 error={formik.touched.description && Boolean(formik.errors.description)}
                 value={formik.values.description}
                 onBlur={formik.handleBlur}
@@ -136,6 +138,7 @@ export const GoodForm = ({
                     id="price"
                     name="price"
                     label="Ціна"
+                    size="small"
                     error={formik.touched.price && Boolean(formik.errors.price)}
                     value={formik.values.price}
                     onBlur={formik.handleBlur}

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

@@ -3,11 +3,57 @@ import { useEffect } from 'react';
 import { connect, useDispatch, useSelector } from 'react-redux';
 import { Route, Routes, useParams } from 'react-router-dom';
 import { actionGoodById } from '../../../actions/actionGoodById';
-import { actionPromiseClear, store } from '../../../reducers';
+import { actionCatById } from '../../../actions/actionCatById';
+import { actionPromiseClear, store, actionFeedCats } from '../../../reducers';
 import { actionFeedAdd, actionFeedClear, actionFeedGoods } from '../../../reducers/feedReducer';
 import { CProtectedRoute } from '../../common/ProtectedRoute';
 import { CAdminGoodPage } from '../AdminGoodPage';
 import { AdminGoodsPage } from '../AdminGoodsPage';
+import { AdminCategoriesPage } from '../AdminCategoriesPage';
+import { CAdminCategoryPage } from '../AdminCategoryPage';
+
+const AdminCategoryPageContainer = ({}) => {
+    const dispatch = useDispatch();
+    const params = useParams();
+    useEffect(() => {
+        if (params._id) {
+            dispatch(actionCatById(params._id, 'adminCatById'));
+        } else {
+            dispatch(actionPromiseClear('adminCatById'));
+        }
+    }, [params._id]);
+    return <CAdminCategoryPage />;
+};
+
+const AdminCategoriesPageContainer = ({ cats }) => {
+    const dispatch = useDispatch();
+    useEffect(() => {
+        dispatch(actionFeedCats(cats?.length || 0));
+        window.onscroll = (e) => {
+            if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
+                const {
+                    feed,
+                    promise: { feedCatAll },
+                } = store.getState();
+                if (feedCatAll.status !== 'PENDING') {
+                    dispatch(actionFeedCats(feed.payload?.length || 0));
+                }
+            }
+        };
+        return () => {
+            dispatch(actionFeedClear());
+            dispatch(actionPromiseClear('feedCatAll'));
+
+            dispatch(actionPromiseClear('categoryUpsert'));
+            window.onscroll = null;
+        };
+    }, []);
+
+    useEffect(() => {
+        if (cats.length) dispatch(actionFeedAdd(cats));
+    }, [cats]);
+    return <AdminCategoriesPage />;
+};
 
 const AdminGoodPageContainer = () => {
     const params = useParams();
@@ -56,6 +102,10 @@ const CAdminGoodsPageContainer = connect((state) => ({ goods: state.promise?.fee
     AdminGoodsPageContainer
 );
 
+const CAdminCategoriesPageContainer = connect((state) => ({ cats: state.promise?.feedCatAll?.payload || [] }))(
+    AdminCategoriesPageContainer
+);
+
 const AdminLayoutPage = () => {
     return (
         <Box className="AdminLayoutPage">
@@ -63,6 +113,9 @@ const AdminLayoutPage = () => {
                 <Route path="/goods/" element={<CAdminGoodsPageContainer />} />
                 <Route path="/good/" element={<AdminGoodPageContainer />} />
                 <Route path="/good/:_id" element={<AdminGoodPageContainer />} />
+                <Route path="/categories/" element={<CAdminCategoriesPageContainer />} />
+                <Route path="/category/" element={<AdminCategoryPageContainer />} />
+                <Route path="/category/:_id" element={<AdminCategoryPageContainer />} />
             </Routes>
         </Box>
     );

+ 18 - 0
src/index.scss

@@ -203,6 +203,24 @@
           }
         }
       }
+
+      & .AdminCategoryPage{
+        & .CategoryForm{
+          width:40%;
+          text-align: left;
+        }
+
+        & .AdminCategoryList{
+          width:90%;
+          display: flex;
+          justify-content: center;
+          flex-wrap: wrap;
+          margin-left: 5%;
+
+
+        }
+      }
+
       & .searchBarWrapper{
         display: flex;
         justify-content: center;