Ver código fonte

fixed admin page

Alex 2 anos atrás
pai
commit
5e8b1b6450

+ 2 - 15
src/actions/ActionCategory.js

@@ -2,7 +2,7 @@ import {actionPromise} from "../reducers/PromiseReducer";
 import {gql} from "./PathDB";
 import {actionCategoryChange, actionCategoryCreate} from "../reducers/CategoryReducer";
 
-//CategoryFind
+//CategoryFind -- parent: null
 export const actionRootCats = () => {
     return actionPromise('rootCats', gql(`query rootCats{
           CategoryFind(query: "[{\\"parent\\": null}]"){
@@ -80,20 +80,7 @@ export const actionCategoryUpsert = (category) => {
     )
 }
 
-//CategoryDelete
-// export const actionCategoryDelete = (_id, name) => {
-//     return actionPromise('categoryDelete', gql(`
-//                 mutation categoryDelete($q: CategoryInput){
-//                 CategoryDelete(category: $q) {
-//                     _id
-//                     createdAt
-//                     name
-//                 }
-//             }`, {q: {_id: _id, name: name}}
-//         )
-//     )
-// }
-
+//CategoryFind
 export const actionAllCategory = () => {
     return actionPromise('allCategory', gql(`query allCategory{
           CategoryFind(query: "[{}]"){

+ 1 - 0
src/actions/ActionCreateGood.js

@@ -1,6 +1,7 @@
 import {actionPromise} from "../reducers/PromiseReducer";
 import {gql} from "./PathDB";
 
+//GoodUpsert
 export const actionGoodUpsert = (good) => {
     return actionPromise('goodUpsert', gql(`mutation goodUpsert($good: GoodInput){
             GoodUpsert(good: $good){

+ 16 - 11
src/actions/ActionGoodFind.js

@@ -2,6 +2,7 @@ import {actionSearchResult} from "../reducers/SearchReducer";
 const {actionPromise} = require("../reducers/PromiseReducer");
 const {gql} = require("./PathDB");
 
+//GoodFindOne
 export const actionGoodFindOne = (_id) =>
     actionPromise('goodFindOne', gql(`query goodFindOne($q :String){
             GoodFindOne(query:  $q){
@@ -13,8 +14,9 @@ export const actionGoodFindOne = (_id) =>
             }
     }`, { q: JSON.stringify([{ _id }]) }))
 
-export const actionGoodFind = (text) =>
-    actionPromise('goodFind', gql(`
+//GoodFind
+export const actionGoodFind = (text) => {
+    return actionPromise('goodFind', gql(`
         query goodFind($query: String){
             GoodFind(query: $query){
                 _id, name, description, price, images{
@@ -23,17 +25,19 @@ export const actionGoodFind = (text) =>
                     _id, name
                 }
             }
-        }`, {query: JSON.stringify([
-                {
-                    $or: [{name: `/${text}/`}, {description: `/${text}/`}]
-                },
-                {
-                    sort: [{title: 1}]
-                }
-            ])}
+        }`, {
+                query: JSON.stringify([
+                    {
+                        $or: [{name: `/${text}/`}, {description: `/${text}/`}]
+                    },
+                    {
+                        sort: [{title: 1}]
+                    }
+                ])
+            }
         )
     )
-
+}
 export const actionFullGoodFind = (text) =>
     async dispatch => {
         let value = await dispatch(actionGoodFind(text))
@@ -42,6 +46,7 @@ export const actionFullGoodFind = (text) =>
         }
     }
 
+//GoodFind - name: {$ne: null}
 export const actionAllGoodFind = () => {
     return actionPromise('goodAllFind', gql(`
         query goodAllFind($query: String){

+ 2 - 3
src/actions/ActionLogin.js

@@ -3,13 +3,12 @@ const {actionAuthLogin} = require("../reducers/AuthReducer");
 const {actionPromise} = require("../reducers/PromiseReducer");
 const {gql} = require("./PathDB");
 
-
+//login
 const actionLogin = (login, password) => {
     return actionPromise('login', gql(`query login($login: String, $password: String){
       login(login: $login, password: $password)
     }`, {login: login, password: password}))
 }
-
 export const actionFullLogin = (login, password) =>
     async dispatch => {
         let token = await dispatch(actionLogin(login, password))
@@ -21,6 +20,7 @@ export const actionFullLogin = (login, password) =>
         }
     }
 
+//register
 export const actionRegister = (login, password) => {
     return actionPromise('register', gql(`mutation register($login:String, $password: String){
       UserUpsert(user:{
@@ -31,7 +31,6 @@ export const actionRegister = (login, password) => {
       }
     }`, {login: login, password: password}))
 }
-
 export const actionFullRegister = (login, password) =>
     async dispatch => {
         let allow = await dispatch(actionRegister(login, password))

+ 2 - 1
src/actions/ActionOrderFind.js

@@ -16,7 +16,8 @@ const actionOrderFind = (count, limit) => {
         }
     }`,
             {
-                query: JSON.stringify([{}, { sort: [{ ["createdAt"]: -1 }], skip: [count || 0], limit: [limit] }])
+                query: JSON.stringify([{}, { sort: [{ ["createdAt"]: -1 }],
+                    skip: [count || 0], limit: [limit] }])
             }
         )
     )

+ 15 - 10
src/actions/ActionUploadFile.js

@@ -12,11 +12,19 @@ export const actionUploadFile = file => {
     }).then(res => res.json()).catch(err => console.log(err)))
 }
 
+export const actionUploadFiles = array =>
+    async dispatch => {
+        for (const file of array) {
+            await dispatch(actionUploadFile(file))
+        }
+    }
+
 export const actionSetAvatar = file =>
      async (dispatch, getState) => {
          let result = await dispatch(actionUploadFile(file))
          if (result) {
-             let value = await dispatch(actionPromise('setAvatar', gql(`mutation setAvatar($myid: String, $imageid: ID){
+             let value = await dispatch(actionPromise('setAvatar',
+                 gql(`mutation setAvatar($myid: String, $imageid: ID){
                     UserUpsert(user:{
                                     _id: $myid, 
                                     avatar: {_id: $imageid}})
@@ -34,7 +42,8 @@ export const actionSetAvatar = file =>
 
 export const actionSetLogin = login =>
     async (dispatch, getState) => {
-        let value = await dispatch(actionPromise('setNewLogin', gql(`mutation setNewLogin($myid: String, $login: String){
+        let value = await dispatch(actionPromise('setNewLogin',
+            gql(`mutation setNewLogin($myid: String, $login: String){
                     UserUpsert(user:{_id: $myid, login: $login}){
                         _id
                     }
@@ -46,7 +55,8 @@ export const actionSetLogin = login =>
 
 export const actionSetNick = nick =>
     async (dispatch, getState) => {
-        let value = await dispatch(actionPromise('setNewNick', gql(`mutation setNewNick($myid: String, $nick: String){
+        let value = await dispatch(actionPromise('setNewNick',
+            gql(`mutation setNewNick($myid: String, $nick: String){
                     UserUpsert(user:{_id: $myid, nick: $nick}){
                         _id
                     }
@@ -58,7 +68,8 @@ export const actionSetNick = nick =>
 
 export const actionSetPassword = password =>
     async (dispatch, getState) => {
-        let value = await dispatch(actionPromise('setNewPassword', gql(`mutation setNewPassword($myid: String, $password: String){
+        let value = await dispatch(actionPromise('setNewPassword',
+            gql(`mutation setNewPassword($myid: String, $password: String){
                     UserUpsert(user:{_id: $myid, password: $password}){
                         _id
                     }
@@ -67,9 +78,3 @@ export const actionSetPassword = password =>
             await dispatch(actionFullUserFindOne(getState().user._id))
         }
     }
-
-//     mutation req2($user: UserInput){
-//     UserUpsert(user:$user){
-//         _id
-//     }
-// }

+ 2 - 2
src/actions/ActionUserFind.js

@@ -21,7 +21,7 @@ export const actionFullUserFindOne = (_id) =>
     }
 
 //UserFind
-export const actionUserFind = (count=0, value='login', sort=-1) => {
+export const actionUserFind = (count=0, limit=100) => {
     return actionPromise('allUsers', gql(`query allUsers($query: String!){
           UserFind(query: $query){
             _id login createdAt nick acl avatar 
@@ -29,7 +29,7 @@ export const actionUserFind = (count=0, value='login', sort=-1) => {
            }
         }`,
             {
-                query: JSON.stringify([{login: {$ne: null}}, { sort: [{ [value]: sort }], skip: [count || 0], limit: [100] }]),
+                query: JSON.stringify([{}, { sort: [{ ["createdAt"]: -1 }], skip: [count || 0], limit: [limit] }]),
             }
         )
     )

+ 1 - 0
src/actions/PathDB.js

@@ -13,5 +13,6 @@ const getGQL = url =>
             throw new Error(JSON.stringify(a.errors))
         return a.data[Object.keys(a.data)[0]]
     }
+
 export const backURL = 'http://shop-roles.asmer.fs.a-level.com.ua'
 export const gql = getGQL(backURL + '/graphql');

+ 3 - 1
src/components/CPRoute.jsx

@@ -1,6 +1,7 @@
 import {connect} from "react-redux";
 import {Redirect} from "react-router-dom";
 import Route from "react-router-dom/es/Route";
+import {actionMyRoute} from "../reducers/RouteReducer";
 
 const RRoute = ({action, component: Component, ...routeProps}) => {
     const WrapperComponent = (componentProps) => {
@@ -9,8 +10,9 @@ const RRoute = ({action, component: Component, ...routeProps}) => {
     }
     return <Route {...routeProps} component={WrapperComponent}/>
 }
+
 export const CRRoute = connect(null,
-    {action: match => ({type: 'ROUTE', match})})(RRoute)
+    {action: actionMyRoute})(RRoute)
 
 const ProtectedRoute =({ fallback='/',
                          roles=['admin'],

+ 60 - 20
src/pages/AdminPage/AdminPage.jsx

@@ -1,12 +1,6 @@
 import {
-    Avatar,
     BottomNavigationAction,
-    Button, Card, CardContent, CardHeader, Checkbox, CircularProgress, Collapse,
-    Container, Divider,
-    FormControl,
-    Grid,
-    InputLabel, MenuItem, Pagination, Select, Switch,
-    TextField,
+    Container,
     useMediaQuery
 } from "@mui/material";
 import Breadcrumb from "../../components/Breadcrumbs";
@@ -16,7 +10,6 @@ import { useTheme } from '@mui/material/styles';
 import AppBar from '@mui/material/AppBar';
 import Tabs from '@mui/material/Tabs';
 import Tab from '@mui/material/Tab';
-import Typography from '@mui/material/Typography';
 import Box from '@mui/material/Box';
 import {useState} from "react";
 import PersonIcon from '@mui/icons-material/Person';
@@ -25,15 +18,21 @@ import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
 import BottomNavigation from '@mui/material/BottomNavigation';
 import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
 import EditIcon from '@mui/icons-material/Edit';
-import {CFindGoodEdit, CGoodEdit, FindGoodEdit} from "./GoodTab";
-import {CClients} from "./ClientsTab";
 import {CCategoryEdit, CCategoryEditTree} from "./CateforyTab";
+import {CClients} from "./ClientsTab/ClientsTab";
+import {CGoodEdit} from "./GoodsTab/GoodEdit";
+import {CFindGoodEdit} from "./GoodsTab/FindGoodEdit";
 
-const defaultTabs = [{icon: PersonIcon, text: 'clients'}, {icon: CategoryIcon, text: 'categories'}, {icon: AutoAwesomeIcon, text: 'products'}]
+const defaultTabs = [
+    {icon: PersonIcon, text: 'clients'},
+    {icon: CategoryIcon, text: 'categories'},
+    {icon: AutoAwesomeIcon, text: 'products'}
+]
 
 const IconHeader = ({Icon}) => {
     return <Icon style={{marginRight: '10px'}} />
 }
+
 function TabPanel(props) {
     const { children, value, index, ...other } = props;
 
@@ -47,7 +46,7 @@ function TabPanel(props) {
         >
             {value === index && (
                 <Box sx={{ p: 3 }}>
-                    <Typography>{children}</Typography>
+                    <Box>{children}</Box>
                 </Box>
             )}
         </div>
@@ -65,12 +64,19 @@ function a11yProps(index) {
     };
 }
 
-
 const SelectBlock = ({Block, FindBlock}) => {
     const [value, setValue] = useState('create');
     return (
         <>
-            <Box sx={{width: '100%', display:'flex', justifyContent: 'center', alignItems: 'center', marginBottom: '20px'}}>
+            <Box
+                sx={{
+                    width: '100%',
+                    display:'flex',
+                    justifyContent: 'center',
+                    alignItems: 'center',
+                    marginBottom: '20px'
+                }}
+            >
                 <BottomNavigation
                     showLabels
                     sx={{width: '100%'}}
@@ -79,8 +85,16 @@ const SelectBlock = ({Block, FindBlock}) => {
                         setValue(newValue);
                     }}
                 >
-                    <BottomNavigationAction value={'create'} label="Create" icon={<AddCircleOutlineIcon />} />
-                    <BottomNavigationAction value={'edit'} label="Edit" icon={<EditIcon />} />
+                    <BottomNavigationAction
+                        value={'create'}
+                        label="Create"
+                        icon={<AddCircleOutlineIcon />}
+                    />
+                    <BottomNavigationAction
+                        value={'edit'}
+                        label="Edit"
+                        icon={<EditIcon />}
+                    />
                 </BottomNavigation>
             </Box>
             {value === 'create' ?
@@ -102,8 +116,20 @@ const FullWidthTabs = () => {
     };
 
     return (
-        <Box sx={{ bgcolor: '#fff', width: '100%', borderRadius: '20px', borderTopLeftRadius: '20px', borderTopRightRadius: '20px', overflow: 'hidden'}}>
-            <AppBar position="static" sx={{boxShadow: 'none'}}>
+        <Box
+            sx={{
+                bgcolor: '#fff',
+                width: '100%',
+                borderRadius: '20px',
+                borderTopLeftRadius: '20px',
+                borderTopRightRadius: '20px',
+                overflow: 'hidden'
+            }}
+        >
+            <AppBar
+                position="static"
+                sx={{boxShadow: 'none'}}
+            >
                 <Tabs
                     value={value}
                     onChange={handleChange}
@@ -114,7 +140,14 @@ const FullWidthTabs = () => {
                     aria-label="full width tabs example"
                 >
                     {defaultTabs.map((item, index) =>
-                        <Tab key={index} icon={<IconHeader Icon={item.icon}/>} iconPosition="start" label={item.text} sx={{borderBottom: '1px solid #dedede'}} {...a11yProps(index)}/>
+                        <Tab
+                            key={index}
+                            icon={<IconHeader Icon={item.icon}/>}
+                            iconPosition="start"
+                            label={item.text}
+                            sx={{borderBottom: '1px solid #dedede'}}
+                            {...a11yProps(index)}
+                        />
                     )}
                 </Tabs>
             </AppBar>
@@ -138,10 +171,17 @@ const FullWidthTabs = () => {
 }
 const AdminPage = () => {
     const matches = useMediaQuery('(max-width:768px)')
+
     return(
         <>
             <Breadcrumb links={['admin']} title={'Dashboard'}/>
-            <main style={{backgroundColor: "#f3f3f3", padding: matches ? "20px 0" : "50px 0", minHeight:'300px'}}>
+            <main
+                style={{
+                    backgroundColor: "#f3f3f3",
+                    padding: matches ? "20px 0" : "50px 0",
+                    minHeight:'300px'
+                }}
+            >
                 <Container maxWidth="lg">
                     <FullWidthTabs/>
                 </Container>

+ 0 - 160
src/pages/AdminPage/ClientsTab.jsx

@@ -1,160 +0,0 @@
-import Typography from "@mui/material/Typography";
-import {useEffect, useState} from "react";
-import {
-    Avatar,
-    Card,
-    CardContent,
-    CardHeader,
-    CircularProgress,
-    Collapse, Divider,
-    FormControl,
-    MenuItem, Pagination,
-    Select
-} from "@mui/material";
-import Box from "@mui/material/Box";
-import {backURL} from "../../actions/PathDB";
-import userDefault from "../../img/header/userDefault.png";
-import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
-import {connect} from "react-redux";
-import {actionUserCount, actionUserFind} from "../../actions/ActionUserFind";
-import {timeCalc} from "../ProductPage/timeCalc";
-
-const ClientsCart = ({_id, login, nick, avatar, createdAt, acl}) => {
-    const ItemAccordion = ({text, size='h6'}) => {
-        return (
-            <Typography
-                variant={size}
-                color='#616161'
-                letterSpacing='1px'
-            >
-                {text}
-            </Typography>
-        )
-    }
-    const [expanded, setExpanded] = useState(false);
-    const handleExpandClick = () => {
-        setExpanded(!expanded);
-    };
-    return (
-        <Card sx={{boxShadow: 'none', border: '1px solid #616161', marginBottom: '20px'}}>
-            <Box display='flex' justifyContent='space-between' alignItems='center' padding='0 20px 0 10px'
-                 style={{cursor:'pointer'}}
-                 onClick={handleExpandClick}
-            >
-                <CardHeader
-                    avatar={
-                        avatar ?
-                            <Avatar alt="User" src={avatar?.url && backURL + '/' + avatar.url}/> :
-                            <Avatar alt="User" src={userDefault}/>
-                    }
-                    title={`Login: ${login || 'login'}  ${nick ? '| Nick: ' + nick : ''} ${(acl && Array.isArray(acl) && acl.length > 1) ? '| Status: ' + acl.slice(1, acl.length).toString() : ''}`}
-                    subheader={timeCalc(+createdAt || 0)}
-                />
-                <ExpandMoreIcon />
-            </Box>
-            <Collapse in={expanded} timeout="auto" unmountOnExit>
-                <CardContent>
-                    <ItemAccordion text={`Login: ${login || 'login'}`}/>
-                    <ItemAccordion size={'body1'} text={`${nick ? 'Nick: ' + nick : ''}`}/>
-                    <ItemAccordion size={'body1'} text={`${(acl && Array.isArray(acl) && acl.length > 1) ? 'Status: ' + acl.slice(1, acl.length).toString() : ''}`}/>
-                    <ItemAccordion size={'body1'} text={'Created at: '+timeCalc(+createdAt || 0)}/>
-                </CardContent>
-            </Collapse>
-        </Card>
-    )
-}
-const Clients = ({usersArr, usersCount, getAllClients, getCountUser}) => {
-    const itemsPerPage = 100
-    const [page, setPage] = useState(1)
-    const [sortValue, setSortValue] = useState('login')
-    const [sortVector, setSortVector] = useState(1)
-    const [sortSelect, setSortSelect] = useState(0)
-
-    useEffect(() => {
-        if (!usersArr && page === 1) getAllClients()
-        if (!usersCount) getCountUser()
-        else {
-            setCount(Math.ceil(+usersCount?.payload / itemsPerPage))
-        }
-    }, [getAllClients, getCountUser, page, sortValue, sortVector, usersArr, usersCount])
-
-    const [count, setCount] = useState(1)
-    const handleChange = (event, value) => {
-        setPage(value);
-        getAllClients(itemsPerPage * value, sortValue, sortVector);
-    }
-    const handleChangeSelect = (event) => {
-        setSortSelect(event.target.value);
-        if (event.target.value === 0){
-            setSortValue('login')
-            setSortVector(1)
-            getAllClients(itemsPerPage*page, sortValue, sortVector)
-        }
-        else if (event.target.value === 1){
-            setSortValue('login')
-            setSortVector(-1)
-            getAllClients(itemsPerPage*page, sortValue, sortVector)
-        }
-        else if (event.target.value === 2){
-            setSortValue('login')
-            setSortVector(1)
-            getAllClients(itemsPerPage*page, sortValue, sortVector)
-        }
-        else if (event.target.value === 3){
-            setSortValue('createdAt')
-            setSortVector(1)
-            getAllClients(itemsPerPage*page, sortValue, sortVector)
-        }
-        else if (event.target.value === 4){
-            setSortValue('createdAt')
-            setSortVector(-1)
-            getAllClients(itemsPerPage*page, sortValue, sortVector)
-        }
-    }
-
-    return (
-        <>
-            <Box marginBottom='20px' width='100%' display='flex' justifyContent='space-between' alignItems='center'>
-                {usersCount?.payload  && <Typography variant='h6' letterSpacing='2px'>Total clients: {usersCount.payload}</Typography>}
-                <FormControl variant="standard">
-                    <Select
-                        labelId="demo-simple-select-label"
-                        id="demo-simple-select"
-                        value={sortSelect}
-                        label="Sort"
-                        onChange={handleChangeSelect}
-                        sx={{textTransform: 'uppercase', color: '#616161'}}
-                    >
-                        <MenuItem value={0}>Default sort</MenuItem>
-                        <MenuItem value={1}>Sort by login: ascending</MenuItem>
-                        <MenuItem value={2}>Sort by login: descending</MenuItem>
-                        <MenuItem value={3}>Sort by created at: ascending</MenuItem>
-                        <MenuItem value={4}>Sort by created at: descending</MenuItem>
-                    </Select>
-                </FormControl>
-            </Box>
-            {!usersArr || !usersArr?.payload ?
-                <Box sx={{height: '100%', width: '100%', display: 'flex', justifyContent:'center', alignItems:'center'}}><CircularProgress color="inherit"/></Box>:
-                Array.isArray(usersArr.payload) && usersArr.payload.map(item => {
-                    return <ClientsCart key={item?._id} _id={item?._id || ''} login={item?.login || 'not found'} nick={item?.nick || ''} createdAt={item?.createdAt || ''} avatar={item?.avatar} acl={item?.acl}/>
-                })
-            }
-            <Box width='100%' flexGrow='0'>
-                <Divider sx={{margin: '20px'}}/>
-                <Box display='flex' justifyContent='center' width='100%'>
-                    <Pagination
-                        count={count}
-                        page={page}
-                        onChange={handleChange}
-                        defaultPage={1}
-                        color="primary"
-                        size="large"
-                        showFirstButton
-                        showLastButton
-                    />
-                </Box>
-            </Box>
-        </>
-    )
-}
-export const CClients = connect(state => ({usersArr: state.promise['allUsers'], usersCount: state.promise['usersCount']}), {getAllClients: actionUserFind, getCountUser: actionUserCount})(Clients)

+ 83 - 0
src/pages/AdminPage/ClientsTab/ClientsCart.jsx

@@ -0,0 +1,83 @@
+import Typography from "@mui/material/Typography";
+import {Avatar, Card, CardContent, CardHeader, Collapse} from "@mui/material";
+import {backURL} from "../../../actions/PathDB";
+import userDefault from "../../../img/header/userDefault.png";
+import {useState} from "react";
+import Box from "@mui/material/Box";
+import {timeCalc} from "../../ProductPage/timeCalc";
+import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import {ItemAvatar} from "./ItemAvatar";
+import {ItemAccordion} from "./ItemAccordion";
+
+export const ClientsCart = ({_id, login, nick, avatar, createdAt, acl}) => {
+    const [expanded, setExpanded] = useState(false);
+    const handleExpandClick = () => {
+        setExpanded(!expanded);
+    };
+    return (
+        <Card
+            sx={{
+                boxShadow: 'none',
+                border: '1px solid #616161',
+                marginBottom: '20px'
+            }}
+        >
+            <Box
+                display='flex'
+                justifyContent='space-between'
+                alignItems='center'
+                padding='0 20px 0 10px'
+                style={{cursor:'pointer'}}
+                onClick={handleExpandClick}
+            >
+                <CardHeader
+                    avatar={
+                        avatar ?
+                            <Avatar
+                                alt="User"
+                                src={avatar?.url && backURL + '/' + avatar.url}
+                            />
+                            :
+                            <Avatar
+                                alt="User"
+                                src={userDefault}
+                            />
+                    }
+                    title={`Login: ${login || 'login'}  ${nick ? '| Nick: ' + nick : ''} 
+                        ${(acl && Array.isArray(acl) && acl.length > 1) ?
+                        '| Status: ' + acl.slice(1, acl.length).toString() : ''}`
+                    }
+                    subheader={timeCalc(+createdAt || 0)}
+                />
+                <ExpandMoreIcon />
+            </Box>
+            <Collapse
+                in={expanded}
+                timeout="auto"
+                unmountOnExit
+            >
+                <CardContent>
+                    <ItemAvatar avatar={avatar}/>
+                    <ItemAccordion
+                        text={`Login: ${login || 'login'}`}
+                    />
+                    <ItemAccordion
+                        size={'body1'}
+                        text={`${nick ? 'Nick: ' + nick : ''}`}
+                    />
+                    <ItemAccordion
+                        size={'body1'}
+                        text={`${(acl && Array.isArray(acl) && acl.length > 1)
+                            ? 'Status: ' + acl.slice(1, acl.length).toString()
+                            : ''}`
+                        }
+                    />
+                    <ItemAccordion
+                        size={'body1'}
+                        text={'Created at: '+timeCalc(+createdAt || 0)}
+                    />
+                </CardContent>
+            </Collapse>
+        </Card>
+    )
+}

+ 98 - 0
src/pages/AdminPage/ClientsTab/ClientsTab.jsx

@@ -0,0 +1,98 @@
+import {useEffect, useState} from "react";
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import {CircularProgress, Divider, Pagination} from "@mui/material";
+import {connect} from "react-redux";
+import {actionUserCount, actionUserFind} from "../../../actions/ActionUserFind";
+import {ClientsCart} from "./ClientsCart";
+
+const Clients = ({usersArr, usersCount, getAllClients, getCountUser}) => {
+    const itemsPerPage = 100
+    const [page, setPage] = useState(1)
+    const [count, setCount] = useState(1)
+
+    useEffect(() => {
+        if (!usersArr && page === 1) getAllClients()
+        if (!usersCount) getCountUser()
+        else {
+            setCount(Math.ceil(+usersCount?.payload / itemsPerPage))
+        }
+    }, [usersArr, usersCount])
+    const handleChange = (event, value) => {
+        setPage(value);
+        getAllClients((itemsPerPage * value)-itemsPerPage, itemsPerPage);
+    }
+
+    return (
+        <>
+            <Box
+                marginBottom='20px'
+                width='100%'
+                display='flex'
+                justifyContent='space-between'
+                alignItems='center'
+            >
+                {usersCount?.payload &&
+                    <Typography
+                        variant='h6'
+                        letterSpacing='2px'
+                    >
+                        Total clients: {usersCount.payload}
+                    </Typography>
+                }
+            </Box>
+            {!usersArr || !usersArr?.payload ?
+                <Box
+                    sx={{
+                        height: '100%',
+                        width: '100%',
+                        display: 'flex',
+                        justifyContent:'center',
+                        alignItems:'center'
+                    }}
+                >
+                    <CircularProgress color="inherit"/>
+                </Box>
+                :
+                Array.isArray(usersArr.payload) && usersArr.payload.map(item => {
+                    return(
+                        <ClientsCart
+                            key={item?._id}
+                            _id={item?._id || ''}
+                            login={item?.login || 'not found'}
+                            nick={item?.nick || ''}
+                            createdAt={item?.createdAt || ''}
+                            avatar={item?.avatar}
+                            acl={item?.acl}
+                        />
+                    )
+                })
+            }
+            <Box
+                width='100%'
+                flexGrow='0'
+            >
+                <Divider sx={{margin: '20px'}}/>
+                <Box
+                    display='flex'
+                    justifyContent='center'
+                    width='100%'
+                >
+                    <Pagination
+                        count={count || 1}
+                        page={page}
+                        onChange={handleChange}
+                        defaultPage={1}
+                        color="primary"
+                        size="large"
+                        showFirstButton
+                        showLastButton
+                    />
+                </Box>
+            </Box>
+        </>
+    )
+}
+export const CClients = connect(state => ({usersArr: state.promise['allUsers'],
+        usersCount: state.promise['usersCount']}),
+    {getAllClients: actionUserFind, getCountUser: actionUserCount})(Clients)

+ 13 - 0
src/pages/AdminPage/ClientsTab/ItemAccordion.jsx

@@ -0,0 +1,13 @@
+import Typography from "@mui/material/Typography";
+
+export const ItemAccordion = ({text, size='h6'}) => {
+    return (
+        <Typography
+            variant={size}
+            color='#616161'
+            letterSpacing='1px'
+        >
+            {text}
+        </Typography>
+    )
+}

+ 18 - 0
src/pages/AdminPage/ClientsTab/ItemAvatar.jsx

@@ -0,0 +1,18 @@
+import {Avatar} from "@mui/material";
+import {backURL} from "../../../actions/PathDB";
+import userDefault from "../../../img/header/userDefault.png";
+
+export const ItemAvatar = ({avatar}) => {
+    return (
+        avatar ?
+            <Avatar
+                alt="User"
+                src={avatar?.url && backURL + '/' + avatar.url}
+            />
+            :
+            <Avatar
+                alt="User"
+                src={userDefault}
+            />
+    )
+}

+ 0 - 354
src/pages/AdminPage/GoodTab.jsx

@@ -1,354 +0,0 @@
-import {useEffect, useState} from "react";
-import {useDropzone} from "react-dropzone";
-import {sortableContainer, sortableElement} from "react-sortable-hoc";
-import {arrayMoveImmutable} from "array-move";
-import Box from "@mui/material/Box";
-import Typography from "@mui/material/Typography";
-import {
-    Button,
-    CircularProgress, Container,
-    Grid, IconButton,
-    InputAdornment,
-    TextField
-} from "@mui/material";
-import {connect} from "react-redux";
-import {actionAllCategory} from "../../actions/ActionCategory";
-import {actionGoodUpsert} from "../../actions/ActionCreateGood";
-import Autocomplete from "@mui/material/Autocomplete";
-import {actionFullGoodFind, actionGoodCount} from "../../actions/ActionGoodFind";
-import {actionUploadFile} from "../../actions/ActionUploadFile";
-import {backURL} from "../../actions/PathDB";
-import {actionClearPromise} from "../../reducers/PromiseReducer";
-import {Link} from "react-router-dom";
-import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
-import SearchIcon from "@material-ui/icons/Search";
-import imgNotFound from "../../img/catalog/imgNotFound.png";
-import {actionSearchRemove} from "../../reducers/SearchReducer";
-
-const GoodEdit = ({entity={images: [], categories: []}, onSave, onFileDrop, fileStatus,
-                      categoryState, actionRootCat, goodCount, goods, actionClear, result}) => {
-    const [state, setState] = useState(entity)
-
-    const {getRootProps, getInputProps, isDragActive} = useDropzone({accept: 'image/*', onDrop: acceptedFiles => {
-            acceptedFiles.forEach(async file => {
-                await onFileDrop(file)
-            })
-        }})
-    const SortableItem = sortableElement(({value}) => {
-        return (
-            <Box key={value?._id} sx={{display: 'flex', justifyContent: 'center',  borderRadius: 2, border: '1px solid #eaeaea', marginBottom: 2, width: 200, height: 200, padding: '5px', boxSizing: 'border-box'}}>
-                <Box sx={{display: 'flex', justifyContent: 'center', minWidth: 0, overflow: 'hidden', position: 'relative'}}>
-                    {value?.url ?
-                        <>
-                            <img src={backURL+ '/' + value.url} style={{display: 'block', width: 'auto', height: '100%', objectFit: 'cover', objectPosition: 'center center'}} alt={value.name}/>
-                        </> :
-                        <Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
-                            <CircularProgress />
-                        </Box>
-                    }
-                </Box>
-            </Box>
-        )
-    });
-    const SortableContainer = sortableContainer(({children}) => {
-        return (
-            <aside style={{display:'flex', justifyContent: 'space-between', flexWrap: 'wrap'}}>
-                {children}
-            </aside>
-        )
-    })
-    const onSortEnd = ({oldIndex, newIndex}) => {
-        setState(({images}) => ({
-            ...state,
-            images: arrayMoveImmutable(images, oldIndex, newIndex),
-        }));
-    }
-
-    const handleClear = () => {
-        setState(entity)
-    }
-    const handleOnSave = () => {
-        let query = {...state}
-        state.images?.length > 0 ? query.images = state.images.map(item => {return {'_id': item['_id']}}) : delete query.images
-        state.categories?.length > 0 ? query.categories = state.categories.map(item => {return {'_id': item['_id'], 'name': item['name']}}) : delete query.categories
-        onSave(query)
-        goodCount()
-    }
-    const handleFullClear = () => {
-        setState(entity)
-        actionClear('goodUpsert')
-        actionClear('uploadFile')
-    }
-
-    useEffect(() => {
-        if(!categoryState) actionRootCat()
-        if(!goods) goodCount()
-        if(fileStatus?.status === 'RESOLVED'){
-            state.images?.length > 0 ?
-                setState({...state, images: [...state.images, fileStatus?.payload]}) :
-                setState({...state, images: [fileStatus?.payload]})
-        }
-    },[categoryState, goods, fileStatus])
-    return (
-        <>
-            {!result ?
-                <>
-                    <Typography variant='h6' letterSpacing='2px' marginBottom='20px'>Total products: {goods?.payload || 0}</Typography>
-                    <Box style={{minHeight: "200px", border: '1px dashed #616161', borderRadius: '20px', padding: '20px'}} {...getRootProps()}>
-                        <input {...getInputProps()} />
-                        {isDragActive ?
-                            <Typography variant='body1' textAlign='center' color='#616161'>Drop the file here ...</Typography> :
-                            <Typography variant='body1' textAlign='center' color='#616161' marginBottom='20px'>Drag 'n' drop image files here, or click to select file</Typography>
-                        }
-                        <SortableContainer axis="xy" onSortEnd={onSortEnd}>
-                            {state.images?.length > 0 && state.images.map((value, index) => (
-                                <SortableItem key={`item-${value?._id || index}`} index={index} value={value} />
-                            ))}
-                        </SortableContainer>
-                    </Box>
-                    <Grid container justifyContent='space-between' marginTop='30px'>
-                        <Grid item xs={5.5}>
-                            <TextField fullWidth id="filled-basic" label="Title product" variant="standard" value={state?.name || ''} onChange={e => setState({...state, name: e.target.value})}/>
-                        </Grid>
-                        <Grid item xs={5.5}>
-                            {categoryState && categoryState?.payload && categoryState.payload?.length > 0 &&
-                                <>
-                                    {state.categories?.length > 0 ?
-                                        <Autocomplete
-                                            multiple
-                                            id="tags-standard"
-                                            options={Object.values(categoryState.payload)}
-                                            defaultValue={state.categories}
-                                            onChange={(event, newValue) => {
-                                                setState({...state, categories: [...newValue]})
-                                            }}
-                                            getOptionLabel={(option) => option?.name || 'no name'}
-                                            key={option => option?.id}
-                                            renderInput={(params) => (
-                                                <TextField
-                                                    {...params}
-                                                    variant="standard"
-                                                    label="Select categories"
-                                                    placeholder="categories"
-                                                />
-                                            )}
-                                        /> :
-                                        <Autocomplete
-                                            multiple
-                                            id="tags-standard"
-                                            options={Object.values(categoryState.payload)}
-                                            onChange={(event, newValue) => {
-                                                setState({...state, categories: [...newValue]})
-                                            }}
-                                            getOptionLabel={(option) => option?.name || 'no name'}
-                                            key={option => option?.id}
-                                            renderInput={(params) => (
-                                                <TextField
-                                                    {...params}
-                                                    variant="standard"
-                                                    label="Select categories"
-                                                    placeholder="categories"
-                                                />
-                                            )}
-                                        />
-                                    }
-                                </>
-                            }
-                        </Grid>
-                    </Grid>
-                    <Grid container justifyContent='space-between' marginTop='30px'>
-                        <Grid item xs={5.5}>
-                            <TextField fullWidth
-                                       id='Price'
-                                       type='number'
-                                       label='Price'
-                                       variant='standard'
-                                       value={state?.price || ''}
-                                       onChange={e => setState({...state, price: parseFloat(e.target.value < 0 ? 0 : e.target.value)})}
-                            />
-                        </Grid>
-                        <Grid item xs={5.5}>
-                            <TextField fullWidth
-                                       id='filled-basic'
-                                       label='Description product'
-                                       variant='standard'
-                                       multiline
-                                       value={state?.description || ''}
-                                       onChange={e => setState({...state, description: e.target.value})}
-                            />
-                        </Grid>
-                    </Grid>
-                    <Grid container justifyContent='space-between' marginTop='30px'>
-                        <Grid item xs={5.5} display='flex' justifyContent='center'>
-                            <Button
-                                fullWidth
-                                onClick={handleClear}
-                                variant="outlined"
-                                color='warning'
-                            >
-                                Clear
-                            </Button>
-                        </Grid>
-                        <Grid item xs={5.5} display='flex' justifyContent='center'>
-                            <Button
-                                fullWidth
-                                variant="outlined"
-                                color='primary'
-                                onClick={handleOnSave}
-                            >
-                                Save
-                            </Button>
-                        </Grid>
-                    </Grid>
-                </> :
-                result?.payload?._id ?
-                    <>
-                        <Box display='flex' alignItems='center' flexDirection='column'>
-                            <Typography variant='h5' letterSpacing='2px' textAlign='center' color='#616161' marginBottom='20px'>Product successfully created!</Typography>
-                            <CheckCircleOutlineIcon sx={{marginBottom: '20px'}}/>
-                            <Link to={`/good/${result.payload._id}`} style={{color:'#616161', marginBottom:'20px'}}>
-                                <Typography variant='h5' letterSpacing='2px' textAlign='center' color='#616161'>View results</Typography>
-                            </Link>
-                            <Button variant='outlined' onClick={handleFullClear}>Add more</Button>
-                        </Box>
-                    </> :
-                    result?.error ?
-                        <Box display='flex' alignItems='center' flexDirection='column'>
-                            <Typography variant='h5' letterSpacing='2px' textAlign='center' color='#f00' marginBottom='20px'>Fatal error, try again!</Typography>
-                            <Button variant='outlined' onClick={handleFullClear}>Add more</Button>
-                        </Box>
-                        :
-                        <Box sx={{ display: 'flex' }}>
-                            <CircularProgress />
-                        </Box>
-            }
-        </>
-    )
-}
-
-export const CGoodEdit = connect(state => ({fileStatus: state.promise['uploadFile'],
-    categoryState: state.promise['allCategory'], goods: state.promise['goodCount'], result: state.promise['goodUpsert']}),
-    {actionRootCat: actionAllCategory, onSave: actionGoodUpsert, goodCount:  actionGoodCount,
-        onFileDrop: actionUploadFile, actionClear: actionClearPromise})(GoodEdit)
-
-const ItemFound = ({item:{_id, name, price, images, description, categories}}) => {
-    let [state, setState] = useState(false)
-
-    return (
-        !state ?
-            <Button fullWidth sx={{display: 'flex', justifyContent:'flex-start'}} onClick={() => setState(true)}>
-                <Box style={{display: 'flex', alignItems: 'center', marginBottom: '30px'}}>
-                    <Box width='60px' height='60px' borderRadius='10px' overflow='hidden' marginRight='60px'
-                         position='relative'>
-                        <img style={{
-                            position: 'absolute',
-                            top: '0',
-                            left: '0',
-                            width: '100%',
-                            height: '100%',
-                            objectFit: 'cover'
-                        }} src={images && Array.isArray(images) && images[0]?.url ? backURL + '/' + images[0].url : imgNotFound}
-                             alt={name}/>
-                    </Box>
-                    <Box sx={{
-                        display: 'flex',
-                        flexDirection: 'column',
-                        justifyContent: 'space-between',
-                        alignItems: 'flex-start'
-                    }}>
-                        <Typography
-                            color='#000'
-                            letterSpacing='1px'
-                            fontFamily='sarif'
-                            fontWeight='600'
-                            variant='h6'
-                        >
-                            {name || 'no name'}
-                        </Typography>
-                        <Typography
-                            letterSpacing='1px'
-                            variant='body1'
-                            fontWeight='300'
-                            color='#616161'
-                            margin='10px 0'
-                            sx={{textTransform: 'capitalize'}}
-                        >
-                            {description?.length > 60 ? 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.' : description}
-                        </Typography>
-                        <Typography
-                            color='#000'
-                            letterSpacing='1px'
-                            variant='body1'
-                            fontWeight='600'
-                        >
-                            ${parseFloat(price).toFixed(2)}
-                        </Typography>
-                    </Box>
-                </Box>
-            </Button>
-        :
-            <Box sx={{marginBottom: '30px', border: '1px solid #616161', borderRadius: '10px', padding: '30px 20px'}}>
-                <CGoodEdit entity={{_id, name, price, images, description, categories}}/>
-                <Button variant='outlined' sx={{marginTop: '30px'}} fullWidth onClick={() => setState(false)}>Cansel</Button>
-            </Box>
-    )
-}
-const NotFound = () => {
-    return (
-        <Typography
-            textAlign='center'
-            color='#000'
-            letterSpacing='1px'
-            variant='body1'
-        >
-            No results found
-        </Typography>
-    )
-}
-const FindGoodEdit = ({searchResult, onSearch, onSearchRemove}) => {
-    const [value, setValue] = useState('')
-    const [click, setClick] = useState(false)
-
-    return (
-        <>
-            <Container maxWidth="md">
-                <Typography
-                    variant='h5'
-                    fontFamily='sarif'
-                    letterSpacing='3px'
-                    marginBottom='30px'
-                    marginTop='30px'
-                    textAlign='center'
-                >
-                    WHICH ITEM TO EDIT?
-                </Typography>
-                <TextField
-                    color={'primary'}
-                    fullWidth
-                    variant="standard"
-                    value={value}
-                    placeholder="Start typing..."
-                    onChange={(event) => {setClick(false); setValue(event.target.value); onSearchRemove()}}
-                    InputProps={{
-                        sx: {padding: '10px', outline:'none', color: '#616161', fontWeight: '300', letterSpacing: '1px', marginBottom: '50px'},
-                        endAdornment: (
-                            <InputAdornment position="end">
-                                <IconButton onClick={() => {setClick(true); onSearchRemove(); onSearch(value)}}>
-                                    <SearchIcon />
-                                </IconButton>
-                            </InputAdornment>
-                        )
-                    }}
-                />
-                {(value !== '' && click) && (searchResult?.searchResult ?
-                        Object.values(searchResult.searchResult).length > 0  ?
-                            Object.values(searchResult.searchResult).map(item => <ItemFound key={item?._id} item={item}/>) : <NotFound/> :
-                        <CircularProgress color="inherit"/>
-                )}
-            </Container>
-        </>
-    )
-}
-
-export const CFindGoodEdit = connect(state=>({searchResult: state.search}),
-    {onSearch: actionFullGoodFind, onSearchRemove: actionSearchRemove})(FindGoodEdit)

+ 81 - 0
src/pages/AdminPage/GoodsTab/FindGoodEdit.jsx

@@ -0,0 +1,81 @@
+import {useState} from "react";
+import {CircularProgress, Container, IconButton, InputAdornment, TextField} from "@mui/material";
+import Typography from "@mui/material/Typography";
+import SearchIcon from "@material-ui/icons/Search";
+import {connect} from "react-redux";
+import {actionFullGoodFind} from "../../../actions/ActionGoodFind";
+import {actionSearchRemove} from "../../../reducers/SearchReducer";
+import {NotFound} from "./NotFound";
+import {ItemFound} from "./ItemFound";
+
+const FindGoodEdit = ({searchResult, onSearch, onSearchRemove}) => {
+    const [value, setValue] = useState('')
+    const [click, setClick] = useState(false)
+
+    return (
+        <>
+            <Container maxWidth="md">
+                <Typography
+                    variant='h5'
+                    fontFamily='sarif'
+                    letterSpacing='3px'
+                    marginBottom='30px'
+                    marginTop='30px'
+                    textAlign='center'
+                >
+                    WHICH ITEM TO EDIT?
+                </Typography>
+                <TextField
+                    color={'primary'}
+                    fullWidth
+                    variant="standard"
+                    value={value}
+                    placeholder="Start typing..."
+                    onChange={(event) => {
+                        setClick(false);
+                        setValue(event.target.value);
+                        onSearchRemove()
+                    }}
+                    InputProps={{
+                        sx: {
+                            padding: '10px',
+                            outline:'none',
+                            color: '#616161',
+                            fontWeight: '300',
+                            letterSpacing: '1px',
+                            marginBottom: '50px'
+                        },
+                        endAdornment: (
+                            <InputAdornment position="end">
+                                <IconButton
+                                    onClick={() => {
+                                        setClick(true);
+                                        onSearchRemove();
+                                        onSearch(value)
+                                    }}
+                                >
+                                    <SearchIcon />
+                                </IconButton>
+                            </InputAdornment>
+                        )
+                    }}
+                />
+                {(value !== '' && click) && (searchResult?.searchResult ?
+                        Object.values(searchResult.searchResult).length > 0  ?
+                            Object.values(searchResult.searchResult)
+                                .map(item =>
+                                    <ItemFound
+                                        key={item?._id}
+                                        item={item}
+                                    />)
+                            :
+                            <NotFound/>
+                        :
+                        <CircularProgress color="inherit"/>
+                )}
+            </Container>
+        </>
+    )
+}
+export const CFindGoodEdit = connect(state=>({searchResult: state.search}),
+    {onSearch: actionFullGoodFind, onSearchRemove: actionSearchRemove})(FindGoodEdit)

+ 396 - 0
src/pages/AdminPage/GoodsTab/GoodEdit.jsx

@@ -0,0 +1,396 @@
+import {useEffect, useState} from "react";
+import {useDropzone} from "react-dropzone";
+import {sortableContainer, sortableElement} from "react-sortable-hoc";
+import Box from "@mui/material/Box";
+import {backURL} from "../../../actions/PathDB";
+import {Button, CircularProgress, Grid, TextField} from "@mui/material";
+import {arrayMoveImmutable} from "array-move";
+import Typography from "@mui/material/Typography";
+import Autocomplete from "@mui/material/Autocomplete";
+import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline";
+import {Link} from "react-router-dom";
+import {connect} from "react-redux";
+import {actionAllCategory} from "../../../actions/ActionCategory";
+import {actionGoodUpsert} from "../../../actions/ActionCreateGood";
+import {actionGoodCount} from "../../../actions/ActionGoodFind";
+import {actionUploadFile, actionUploadFiles} from "../../../actions/ActionUploadFile";
+import {actionClearPromise} from "../../../reducers/PromiseReducer";
+
+const GoodEdit = ({entity={images: [], categories: []},
+                      onSave,
+                      onFileDrop,
+                      fileStatus,
+                      categoryState,
+                      actionRootCat,
+                      goodCount,
+                      goods,
+                      actionClear,
+                      result}) => {
+    const [state, setState] = useState(entity)
+
+    const {getRootProps, getInputProps, isDragActive} = useDropzone({
+        accept: 'image/*', onDrop: acceptedFiles => {
+            // acceptedFiles.forEach(async file => {
+            //     await onFileDrop(file)
+            // })
+            onFileDrop(acceptedFiles)
+        }})
+    const SortableItem = sortableElement(({value}) => {
+        return (
+            <Box
+                key={value?._id}
+                sx={{
+                    display: 'flex',
+                    justifyContent: 'center',
+                    borderRadius: 2,
+                    border: '1px solid #eaeaea',
+                    marginBottom: 2,
+                    width: 200,
+                    height: 200,
+                    padding: '5px',
+                    boxSizing: 'border-box'
+                }}
+            >
+                <Box
+                    sx={{
+                        display: 'flex',
+                        justifyContent: 'center',
+                        minWidth: 0,
+                        overflow: 'hidden',
+                        position: 'relative'
+                    }}
+                >
+                    {value?.url ?
+                        <>
+                            <img
+                                src={backURL+ '/' + value.url}
+                                style={{
+                                    display: 'block',
+                                    width: 'auto',
+                                    height: '100%',
+                                    objectFit: 'cover',
+                                    objectPosition: 'center center'
+                                }}
+                                alt={value.name}
+                            />
+                        </> :
+                        <Box
+                            sx={{
+                                display: 'flex',
+                                justifyContent: 'center',
+                                alignItems: 'center'
+                            }}
+                        >
+                            <CircularProgress />
+                        </Box>
+                    }
+                </Box>
+            </Box>
+        )
+    });
+    const SortableContainer = sortableContainer(({children}) => {
+        return (
+            <aside
+                style={{
+                    display:'flex',
+                    justifyContent: 'space-between',
+                    flexWrap: 'wrap'
+                }}
+            >
+                {children}
+            </aside>
+        )
+    })
+    const onSortEnd = ({oldIndex, newIndex}) => {
+        setState(({images}) => ({
+            ...state,
+            images: arrayMoveImmutable(images, oldIndex, newIndex),
+        }));
+    }
+
+    const handleClear = () => {
+        setState(entity)
+    }
+    const handleOnSave = () => {
+        let query = {...state}
+        state.images?.length > 0 ?
+            query.images = state.images.map(item => {return {'_id': item['_id']}})
+            :
+            delete query.images
+        state.categories?.length > 0 ?
+            query.categories = state.categories.map(item => {return {'_id': item['_id'], 'name': item['name']}})
+            :
+            delete query.categories
+        onSave(query)
+        goodCount()
+    }
+    const handleFullClear = () => {
+        setState(entity)
+        actionClear('goodUpsert')
+        actionClear('uploadFile')
+    }
+
+    useEffect(() => {
+        if(!categoryState) actionRootCat()
+        if(!goods) goodCount()
+        if(fileStatus?.status === 'RESOLVED'){
+            state.images?.length > 0 ?
+                setState({...state, images: [...state.images, fileStatus?.payload]})
+                :
+                setState({...state, images: [fileStatus?.payload]})
+        }
+    },[categoryState, goods, fileStatus])
+    return (
+        <>
+            {!result ?
+                <>
+                    <Typography
+                        variant='h6'
+                        letterSpacing='2px'
+                        marginBottom='20px'
+                    >
+                        Total products: {goods?.payload || 0}
+                    </Typography>
+                    <Box
+                        style={{
+                            minHeight: "200px",
+                            border: '1px dashed #616161',
+                            borderRadius: '20px',
+                            padding: '20px'
+                        }}
+                        {...getRootProps()}
+                    >
+                        <input {...getInputProps()} />
+                        {isDragActive ?
+                            <Typography
+                                variant='body1'
+                                textAlign='center'
+                                color='#616161'
+                            >
+                                Drop the file here ...
+                            </Typography>
+                            :
+                            <Typography
+                                variant='body1'
+                                textAlign='center'
+                                color='#616161'
+                                marginBottom='20px'
+                            >
+                                Drag 'n' drop image files here, or click to select file
+                            </Typography>
+                        }
+                        <SortableContainer
+                            axis="xy"
+                            onSortEnd={onSortEnd}
+                        >
+                            {state.images?.length > 0 && state.images.map((value, index) => (
+                                <SortableItem
+                                    key={`item-${value?._id || index}`}
+                                    index={index}
+                                    value={value}
+                                />
+                            ))}
+                        </SortableContainer>
+                    </Box>
+                    <Grid
+                        container
+                        justifyContent='space-between'
+                        alignItems='flex-end'
+                        marginTop='30px'
+                    >
+                        <Grid item xs={5.5}>
+                            <TextField
+                                fullWidth
+                                id="filled-basic"
+                                label="Title product"
+                                variant="standard"
+                                value={state?.name || ''}
+                                onChange={e => setState({...state, name: e.target.value})}
+                            />
+                        </Grid>
+                        <Grid item xs={5.5}>
+                            {categoryState && categoryState?.payload && categoryState.payload?.length > 0 &&
+                                <>
+                                    {state.categories?.length > 0 ?
+                                        <Autocomplete
+                                            multiple
+                                            id="tags-standard"
+                                            options={Object.values(categoryState.payload)}
+                                            defaultValue={state.categories}
+                                            onChange={(event, newValue) => {
+                                                setState({...state, categories: [...newValue]})
+                                            }}
+                                            getOptionLabel={(option) => option?.name || 'no name'}
+                                            key={option => option?.id}
+                                            renderInput={(params) => (
+                                                <TextField
+                                                    {...params}
+                                                    variant="standard"
+                                                    label="Select categories"
+                                                    placeholder="categories"
+                                                />
+                                            )}
+                                        /> :
+                                        <Autocomplete
+                                            multiple
+                                            id="tags-standard"
+                                            options={Object.values(categoryState.payload)}
+                                            onChange={(event, newValue) => {
+                                                setState({...state, categories: [...newValue]})
+                                            }}
+                                            getOptionLabel={(option) => option?.name || 'no name'}
+                                            key={option => option?.id}
+                                            renderInput={(params) => (
+                                                <TextField
+                                                    {...params}
+                                                    variant="standard"
+                                                    label="Select categories"
+                                                    placeholder="categories"
+                                                />
+                                            )}
+                                        />
+                                    }
+                                </>
+                            }
+                        </Grid>
+                    </Grid>
+                    <Grid
+                        container
+                        justifyContent='space-between'
+                        alignItems='flex-end'
+                        marginTop='30px'
+                    >
+                        <Grid item xs={5.5}>
+                            <TextField fullWidth
+                                       id='Price'
+                                       type='number'
+                                       label='Price'
+                                       variant='standard'
+                                       value={state?.price || ''}
+                                       onChange={e => setState({...state,
+                                           price: parseFloat(e.target.value < 0 ? 0 : e.target.value)})
+                                       }
+                            />
+                        </Grid>
+                        <Grid item xs={5.5}>
+                            <TextField fullWidth
+                                       id='filled-basic'
+                                       label='Description product'
+                                       variant='standard'
+                                       multiline
+                                       value={state?.description || ''}
+                                       onChange={e => setState({...state, description: e.target.value})}
+                            />
+                        </Grid>
+                    </Grid>
+                    <Grid
+                        container
+                        justifyContent='space-between'
+                        marginTop='30px'
+                    >
+                        <Grid
+                            item xs={5.5}
+                            display='flex'
+                            justifyContent='center'
+                        >
+                            <Button
+                                fullWidth
+                                onClick={handleClear}
+                                variant="outlined"
+                                color='warning'
+                            >
+                                Clear
+                            </Button>
+                        </Grid>
+                        <Grid
+                            item xs={5.5}
+                            display='flex'
+                            justifyContent='center'
+                        >
+                            <Button
+                                fullWidth
+                                variant="outlined"
+                                color='primary'
+                                onClick={handleOnSave}
+                            >
+                                Save
+                            </Button>
+                        </Grid>
+                    </Grid>
+                </> :
+                result?.payload?._id ?
+                    <>
+                        <Box
+                            display='flex'
+                            alignItems='center'
+                            flexDirection='column'
+                        >
+                            <Typography
+                                variant='h5'
+                                letterSpacing='2px'
+                                textAlign='center'
+                                color='#616161'
+                                marginBottom='20px'
+                            >
+                                Product successfully created!
+                            </Typography>
+                            <CheckCircleOutlineIcon sx={{marginBottom: '20px'}}/>
+                            <Link
+                                to={`/good/${result.payload._id}`}
+                                style={{
+                                    color:'#616161',
+                                    marginBottom:'20px'
+                                }}
+                            >
+                                <Typography
+                                    variant='h5'
+                                    letterSpacing='2px'
+                                    textAlign='center'
+                                    color='#616161'
+                                >
+                                    View results
+                                </Typography>
+                            </Link>
+                            <Button
+                                variant='outlined'
+                                onClick={handleFullClear}
+                            >
+                                Add more
+                            </Button>
+                        </Box>
+                    </> :
+                    result?.error ?
+                        <Box
+                            display='flex'
+                            alignItems='center'
+                            flexDirection='column'
+                        >
+                            <Typography
+                                variant='h5'
+                                letterSpacing='2px'
+                                textAlign='center'
+                                color='#f00'
+                                marginBottom='20px'
+                            >
+                                Fatal error, try again!
+                            </Typography>
+                            <Button
+                                variant='outlined'
+                                onClick={handleFullClear}
+                            >
+                                Add more
+                            </Button>
+                        </Box>
+                        :
+                        <Box sx={{ display: 'flex' }}>
+                            <CircularProgress />
+                        </Box>
+            }
+        </>
+    )
+}
+
+export const CGoodEdit = connect(state => ({fileStatus: state.promise['uploadFile'],
+        categoryState: state.promise['allCategory'], goods: state.promise['goodCount'], result: state.promise['goodUpsert']}),
+    {actionRootCat: actionAllCategory, onSave: actionGoodUpsert, goodCount:  actionGoodCount,
+        onFileDrop: actionUploadFiles, actionClear: actionClearPromise})(GoodEdit)

+ 114 - 0
src/pages/AdminPage/GoodsTab/ItemFound.jsx

@@ -0,0 +1,114 @@
+import {useState} from "react";
+import {Button} from "@mui/material";
+import Box from "@mui/material/Box";
+import {backURL} from "../../../actions/PathDB";
+import imgNotFound from "../../../img/catalog/imgNotFound.png";
+import Typography from "@mui/material/Typography";
+import {CGoodEdit} from "./GoodEdit";
+
+export const ItemFound = ({item:{_id, name, price, images, description, categories}}) => {
+    let [state, setState] = useState(false)
+
+    return (
+        !state ?
+            <Button
+                fullWidth
+                sx={{
+                    display: 'flex',
+                    justifyContent:'flex-start'
+                }}
+                onClick={() => setState(true)}
+            >
+                <Box
+                    style={{
+                        display: 'flex',
+                        alignItems: 'center',
+                        marginBottom: '30px'
+                    }}
+                >
+                    <Box
+                        width='60px'
+                        height='60px'
+                        borderRadius='10px'
+                        overflow='hidden'
+                        marginRight='60px'
+                        position='relative'
+                    >
+                        <img
+                            style={{
+                                position: 'absolute',
+                                top: '0',
+                                left: '0',
+                                width: '100%',
+                                height: '100%',
+                                objectFit: 'cover'
+                            }}
+                            src={images && Array.isArray(images) && images[0]?.url ?
+                                backURL + '/' + images[0].url : imgNotFound
+                            }
+                            alt={name}
+                        />
+                    </Box>
+                    <Box sx={{
+                        display: 'flex',
+                        flexDirection: 'column',
+                        justifyContent: 'space-between',
+                        alignItems: 'flex-start'
+                    }}>
+                        <Typography
+                            color='#000'
+                            letterSpacing='1px'
+                            fontFamily='sarif'
+                            fontWeight='600'
+                            variant='h6'
+                        >
+                            {name || 'no name'}
+                        </Typography>
+                        <Typography
+                            letterSpacing='1px'
+                            variant='body1'
+                            fontWeight='300'
+                            color='#616161'
+                            margin='10px 0'
+                            sx={{textTransform: 'capitalize'}}
+                        >
+                            {description?.length > 60 ?
+                                'Lorem ipsum dolor sit amet, consectetur adipisicing elit.'
+                                :
+                                description
+                            }
+                        </Typography>
+                        <Typography
+                            color='#000'
+                            letterSpacing='1px'
+                            variant='body1'
+                            fontWeight='600'
+                        >
+                            ${parseFloat(price).toFixed(2)}
+                        </Typography>
+                    </Box>
+                </Box>
+            </Button>
+            :
+            <Box
+                sx={{
+                    marginBottom: '30px',
+                    border: '1px solid #616161',
+                    borderRadius: '10px',
+                    padding: '30px 20px'
+                }}
+            >
+                <CGoodEdit
+                    entity={{_id, name, price, images, description, categories}}
+                />
+                <Button
+                    variant='outlined'
+                    sx={{marginTop: '30px'}}
+                    fullWidth
+                    onClick={() => setState(false)}
+                >
+                    Cansel
+                </Button>
+            </Box>
+    )
+}

+ 14 - 0
src/pages/AdminPage/GoodsTab/NotFound.jsx

@@ -0,0 +1,14 @@
+import Typography from "@mui/material/Typography";
+
+export const NotFound = () => {
+    return (
+        <Typography
+            textAlign='center'
+            color='#000'
+            letterSpacing='1px'
+            variant='body1'
+        >
+            No results found
+        </Typography>
+    )
+}

+ 3 - 1
src/reducers/CombineReducers.js

@@ -8,6 +8,7 @@ import {WishListReducer} from "./WishListReducer";
 import {localStoredReducer} from "./LocalStoredReducer";
 import {SearchReducer} from "./SearchReducer";
 import {MyOrdersReducer} from "./MyOrdersReducer";
+import {RouteReducer} from "./RouteReducer";
 
 export const rootReducer = combineReducers({
     auth: AuthReducer,
@@ -17,5 +18,6 @@ export const rootReducer = combineReducers({
     category: CategoryReducer,
     wishlist: localStoredReducer(WishListReducer, 'wishlist'),
     search: SearchReducer,
-    myorders: MyOrdersReducer
+    myorders: MyOrdersReducer,
+    route: RouteReducer,
 })

+ 12 - 0
src/reducers/RouteReducer.js

@@ -0,0 +1,12 @@
+export const RouteReducer = (state={}, { type, match }) => {
+    if (type === 'ROUTE_ADD') {
+        return match
+    }
+    if (type === 'ROUTE_CLEAR'){
+        return {}
+    }
+    return state
+}
+
+export const actionMyRoute = match => ({type: 'ROUTE_ADD', match})
+export const actionMyRouteClear = () => ({type: 'ROUTE_CLEAR'})