commit 4d2f04f48f5b2505cbc0abe8de932913c1058d80
Author: Schwobik <michal.schwob@seznam.cz>
Date:   Sun May 8 22:25:57 2022 +0200

    Creation of administration page and user registration
    re #9627

diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml
index 4d73a3d..ae0f936 100644
--- a/backend/docker-compose.yml
+++ b/backend/docker-compose.yml
@@ -19,7 +19,7 @@ services:
   app: # Spring boot application
     build: .
     container_name: app-backend  # name of the container
-    image: schwobik/backend-app:1.2
+    image: schwobik/backend-app:1.5
     ports:
       - "8080:8080"                 # expose port 8080 out of the docker container do the local machine
     depends_on:
@@ -31,7 +31,7 @@ services:
 #     Since our Dockerfile for web-server is located in react-app folder, our build context is ./react-app
     build: ../frontend
     container_name: frontend
-    image: schwobik/frontend-app:1.2
+    image: schwobik/frontend-app:1.5
     ports:
       - "80:80"
 
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 74b5e05..f591d29 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -36,3 +36,8 @@
     transform: rotate(360deg);
   }
 }
+
+.clicked-user {
+  background: #A28253;
+  color: white;
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index e6103ee..eefb1c7 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -16,6 +16,8 @@ import ThemeWrapper from './features/Theme/ThemeWrapper'
 import EditHome from "./features/Home/EditHome"
 import Notification from './features/Notification/Notification'
 import { Fragment } from 'react'
+import Administration from "./features/Administration/Administration"
+import Register from "./features/Auth/Register"
 
 const App = () => {
     return (
@@ -33,8 +35,10 @@ const App = () => {
                                 element={<CatalogItemDetail />}
                             />
                             <Route path="/login" element={<Login />} />
+                            <Route path="/register" element={<Register />} />
                             <Route path="/logout" element={<Logout />} />
                             <Route path="/map" element={<TrackingTool />} />
+                            <Route path="/administration" element={<Administration />}/>
                             <Route path="*" element={<NotFound />} />
                         </Routes>
                     </Box>
diff --git a/frontend/src/features/Administration/Administration.tsx b/frontend/src/features/Administration/Administration.tsx
new file mode 100644
index 0000000..b27a2c7
--- /dev/null
+++ b/frontend/src/features/Administration/Administration.tsx
@@ -0,0 +1,122 @@
+import {
+    Button,
+    Container, Grid,
+    Link,
+    List,
+    ListItem,
+    ListItemIcon,
+    ListItemText,
+    NativeSelect,
+    Paper,
+    Select, SelectChangeEvent
+} from '@mui/material'
+import {Fragment, ReactNode, useEffect, useState} from 'react'
+import { useDispatch, useSelector } from 'react-redux'
+import { RootState } from '../redux/store'
+import axiosInstance from "../../api/api"
+import 'react-quill/dist/quill.snow.css'
+import ContentLoading from "../Reusables/ContentLoading"
+import {UserDto} from "../../swagger/data-contracts"
+import UserDetail from "./UserDetail"
+import EditIcon from "@mui/icons-material/Edit"
+import {Link as RouterLink} from "react-router-dom"
+import AddIcon from '@mui/icons-material/Add';
+
+
+
+const apiError =
+    'Error while fetching data from the server, please try again later.'
+
+const Administration = () => {
+    const [users, setUsers] = useState<UserDto[] | undefined>(undefined)
+    const [selectedUser, setSelectedUser] = useState<UserDto | undefined>(undefined)
+    const [selectedUserID, setSelectedUserID] = useState<number | undefined>(undefined)
+    const [isContentLoading, setIsContentLoading] = useState(true)
+    const [err, setErr] = useState<string | undefined>(undefined)
+
+    const isAdmin = useSelector(
+        (state: RootState) => state.user.roles.includes("ROLE_ADMIN")
+    )
+
+    const handleClick = (userID: number) => {
+        console.log("clicked " + userID)
+        if (users) {
+            setSelectedUser(users.at(userID))
+            setSelectedUserID(userID)
+        }
+    }
+
+    // Function to fetch the item from the api
+    const fetchItem = async () => {
+        try {
+            const { data, status } = await axiosInstance.get(
+                `/users`
+            )
+            if (status !== 200) {
+                setErr(apiError)
+                return
+            }
+
+            setUsers(data)
+            setIsContentLoading(false)
+        } catch (err: any) {
+            setErr(apiError)
+        }
+    }
+
+    // Fetch the item from the api after mounting the component
+    useEffect(() => {
+        fetchItem()
+    }, [])
+
+    const childClosed = () => {
+        setSelectedUser(undefined)
+        setSelectedUserID(undefined)
+        fetchItem()
+    }
+
+    return (
+        <Fragment>
+
+            {isContentLoading && !err ? <ContentLoading /> : null}
+            {!isContentLoading && users && isAdmin ? (
+                <Grid container justifyContent="space-around">
+                    <Grid item xs={3} md={2} sx={{ px: 2 }}>
+                        <Paper style={{ minHeight: '80vh', display:'flex', justifyContent: 'space-between', flexDirection:'column'}} variant="outlined">
+                            <List>
+                                {users.map((user, id) => (
+                                    <ListItem
+                                        key={id}
+                                        className={
+                                            selectedUserID === id
+                                                ? 'clicked-user'
+                                                : ''
+                                        }
+                                        onClick={() => handleClick(id)}
+                                    >
+                                        <ListItemText primary={user.name} />
+                                    </ListItem>
+                                ))}
+                            </List>
+                            <Button startIcon={<AddIcon />}
+                                    variant="contained"
+                                    component={RouterLink}
+                                    to="/register"
+                                    color="primary"
+                                    sx={{ m: 2 }} >
+                                Add user
+                            </Button>
+                        </Paper>
+                    </Grid>
+                    <Grid item md={10} xs={9}>
+                        {selectedUser ?
+                            <UserDetail user={selectedUser} onClose={childClosed} />
+                        : null }
+                    </Grid>
+                </Grid>
+            ) : null}
+        </Fragment>
+    )
+}
+
+export default Administration
diff --git a/frontend/src/features/Administration/UserDetail.tsx b/frontend/src/features/Administration/UserDetail.tsx
new file mode 100644
index 0000000..3731047
--- /dev/null
+++ b/frontend/src/features/Administration/UserDetail.tsx
@@ -0,0 +1,136 @@
+import {
+    Button,
+    Container, Divider, Grid,
+    Checkbox,
+    Link,
+    List,
+    ListItem,
+    ListItemIcon,
+    ListItemText,
+    NativeSelect,
+    Paper,
+    Select, SelectChangeEvent, Typography, FormGroup, FormControlLabel
+} from '@mui/material'
+import React, {ChangeEvent, Fragment, ReactNode, useEffect, useState} from 'react'
+import {useDispatch, useSelector} from 'react-redux'
+import {RootState} from '../redux/store'
+import axiosInstance from "../../api/api"
+import 'react-quill/dist/quill.snow.css'
+import ContentLoading from "../Reusables/ContentLoading"
+import {Link as RouterLink, useNavigate} from "react-router-dom"
+import EditIcon from '@mui/icons-material/Edit'
+import {formatHtmlStringToReactDom} from "../../utils/formatting/HtmlUtils"
+import {PermissionDto, UserDto} from "../../swagger/data-contracts"
+import {Label} from "@mui/icons-material"
+import ClearIcon from "@mui/icons-material/Clear"
+
+export interface UserDetailProps {
+    user: UserDto,
+    onClose: () => void
+}
+
+const UserDetail = (props: UserDetailProps) => {
+    const [user] = useState(props.user)
+    const [canRead, setCanRead] = useState(user.permissions?.canRead)
+    const [canWrite, setCanWrite] = useState(user.permissions?.canWrite)
+    const [canDelete, setCanDelete] = useState(user.permissions?.canDelete)
+
+
+    // Maps user property to corresponding table row
+    const mapToRow = (rowName: string, item: string) => (
+        <Fragment>
+            <Grid sx={{my: 2}} container justifyContent="space-around">
+                <Grid item xs={4} sx={{px: 1}}>
+                    <Typography fontWeight={500}>{rowName}</Typography>
+                </Grid>
+                <Grid item xs={8} sx={{ml: 'auto'}}>
+                    <Typography key={item}>{item}</Typography>
+                </Grid>
+            </Grid>
+        </Fragment>
+    )
+
+    const rows = [
+        {
+            rowName: 'Name:',
+            item: [props.user?.name],
+        },
+        {
+            rowName: 'E-mail:',
+            item: user?.email,
+        }
+    ]
+    const rights = {
+        canWrite: 'Add and edit locations',
+        canDelete: 'Delete locations',
+        canRead: 'Access to tracking tool',
+    }
+
+
+    const deleteUser = async () => {
+        const { data, status } = await axiosInstance.delete(
+            `/users/${user.email}`
+        )
+        if (status !== 200) {
+            // TODO dodělat zpracování erroru
+            return
+        }
+        props.onClose();
+    }
+
+    return (
+        <Fragment>
+            <Paper style={{minHeight: '80vh'}} variant="outlined">
+                <Grid container justifyContent="space-around">
+                    <Grid item xs={6} md={6} sx={{p: 2}}>
+                        <Typography fontWeight={800}>Information</Typography>
+                        {rows.map((row) => (
+
+                            <Fragment>
+                                {mapToRow(
+                                    row.rowName as string,
+                                    row.item as string
+                                )}
+                            </Fragment>
+                        ))}
+                    </Grid>
+                    <Grid item xs={6} md={6} sx={{p: 2}}>
+                        <b>Rights</b>
+                            <FormGroup>
+                                <FormControlLabel
+                                    control={
+                                        <Checkbox
+                                            checked={canWrite}
+                                            onChange={e => setCanWrite(e.target.checked)}
+                                        />}
+                                    label={rights.canWrite}/>
+                                <FormControlLabel
+                                    control={
+                                        <Checkbox
+                                            checked={canRead}
+                                            onChange={e => setCanRead(e.target.checked)}
+                                        />}
+                                    label={rights.canRead}/>
+                                <FormControlLabel
+                                    control={
+                                        <Checkbox
+                                            checked={canDelete}
+                                            onChange={e => setCanDelete(e.target.checked)}
+                                        />}
+                                    label={rights.canDelete}/>
+                            </FormGroup>
+                    </Grid>
+                </Grid>
+                <Button startIcon={<ClearIcon />}
+                        variant="contained"
+                        color="primary"
+                        onClick={deleteUser}
+                        sx={{ m: 2 }} >
+                    Delete
+                </Button>
+            </Paper>
+        </Fragment>
+    )
+}
+
+export default UserDetail
\ No newline at end of file
diff --git a/frontend/src/features/Auth/Register.tsx b/frontend/src/features/Auth/Register.tsx
index 4833a02..255712f 100644
--- a/frontend/src/features/Auth/Register.tsx
+++ b/frontend/src/features/Auth/Register.tsx
@@ -1,2 +1,22 @@
+import { Fragment, useEffect } from 'react'
+import { useSelector } from 'react-redux'
+import { useNavigate } from 'react-router-dom'
+import { RootState } from '../redux/store'
+import LoginDialog from './LoginDialog'
+import NotAuthorized from "../NotAuthorized/NotAuthorized"
+import RegisterDialog from "./RegisterDialog"
 
-export default {}
\ No newline at end of file
+
+const Register = () => {
+    const isAdmin = useSelector(
+        (state: RootState) => state.user.roles.includes("ROLE_ADMIN")
+    )
+
+    return (
+        <Fragment>
+            {isAdmin ? (<RegisterDialog />) : <NotAuthorized />}
+        </Fragment>
+    )
+}
+
+export default Register
diff --git a/frontend/src/features/Auth/RegisterDialog.tsx b/frontend/src/features/Auth/RegisterDialog.tsx
index 8833c33..a8fcc8a 100644
--- a/frontend/src/features/Auth/RegisterDialog.tsx
+++ b/frontend/src/features/Auth/RegisterDialog.tsx
@@ -1,6 +1,174 @@
-import { useFormik } from "formik"
+import { Fragment, FunctionComponent, useEffect, useState } from 'react'
+import Dialog, { DialogProps } from '@mui/material/Dialog'
+import {
+    Button,
+    DialogContent,
+    Link,
+    TextField,
+    Typography,
+} from '@mui/material'
+import { useFormik } from 'formik'
+import * as yup from 'yup'
+import { useDispatch, useSelector } from 'react-redux'
+import { Link as RouterLink, useNavigate } from 'react-router-dom'
+import {logIn, register} from './userThunks'
+import { RootState } from '../redux/store'
+import { resetLoggingIn } from './userSlice'
+import { Box } from '@mui/system'
 
-const register = () => {
+export interface RegisterDialogProps {
+    maxWidth?: DialogProps['maxWidth']
+}
+
+const RegisterDialog: FunctionComponent<RegisterDialogProps> = ({
+                                                              maxWidth,
+                                                          }) => {
+    const [open, setOpen] = useState(true)
+
+    const dispatch = useDispatch()
+    const navigate = useNavigate()
+    dispatch(resetLoggingIn())
+    const validationSchema = yup.object().shape({
+        username: yup.string().required('Username is required'),
+        password: yup.string().required('Password is required'),
+        confirmationPassword: yup.string().oneOf([yup.ref('password'), null], 'Passwords must match'),
+        email: yup.string().required('Email is required'),
+
+    })
 
-    return <></>
+    const formik = useFormik({
+        initialValues: {
+            username: '',
+            password: '',
+            confirmationPassword: '',
+            email: '',
+        },
+        validationSchema,
+        onSubmit: () => {
+            dispatch(
+                register({
+                    username: formik.values.username,
+                    passwords: {
+                        password: formik.values.password,
+                        confirmationPassword: formik.values.confirmationPassword,
+                    },
+                    permissions: {
+                        canRead: false,
+                        canWrite: false,
+                        canDelete: false,
+                    },
+                    email: formik.values.email,
+                })
+            )
+        },
+    })
+
+    const onCancel = () => {
+        formik.resetForm()
+        navigate('/Administration')
+    }
+
+    return (
+        <Fragment>
+            <Dialog
+                open={open}
+                fullWidth={true}
+                onClose={onCancel}
+                maxWidth="md"
+            >
+                <Typography sx={{ ml: 2, mt: 2 }} variant="h5" fontWeight="600">
+                    Register new user
+                </Typography>
+                <DialogContent>
+                    <form onSubmit={formik.handleSubmit}>
+                        <TextField
+                            fullWidth
+                            label="Name"
+                            name="username"
+                            sx={{ mb: 2 }}
+                            value={formik.values.username}
+                            onChange={formik.handleChange}
+                            error={
+                                Boolean(formik.errors.username) &&
+                                formik.touched.username
+                            }
+                            helperText={
+                                formik.errors.username &&
+                                formik.touched.username &&
+                                formik.errors.username
+                            }
+                        />
+                        <TextField
+                            fullWidth
+                            label="E-mail"
+                            name="email"
+                            type="email"
+                            sx={{ mb: 2 }}
+                            value={formik.values.email}
+                            onChange={formik.handleChange}
+                            error={
+                                Boolean(formik.errors.email) &&
+                                formik.touched.email
+                            }
+                            helperText={
+                                formik.errors.email &&
+                                formik.touched.email &&
+                                formik.errors.email
+                            }
+                        />
+                        <TextField
+                            fullWidth
+                            label="Password"
+                            name="password"
+                            type="password"
+                            sx={{ mb: 2 }}
+                            value={formik.values.password}
+                            onChange={formik.handleChange}
+                            error={
+                                Boolean(formik.errors.password) &&
+                                formik.touched.password
+                            }
+                            helperText={
+                                formik.errors.password &&
+                                formik.touched.password &&
+                                formik.errors.password
+                            }
+                        />
+                        <TextField
+                            fullWidth
+                            label="Confirm Password"
+                            name="confirmationPassword"
+                            type="password"
+                            sx={{ mb: 2 }}
+                            value={formik.values.confirmationPassword}
+                            onChange={formik.handleChange}
+                            error={
+                                Boolean(formik.errors.confirmationPassword) &&
+                                formik.touched.confirmationPassword
+                            }
+                            helperText={
+                                formik.errors.confirmationPassword &&
+                                formik.touched.confirmationPassword &&
+                                formik.errors.confirmationPassword
+                            }
+                        />
+                        <Fragment>
+                            <Button
+                                sx={{mb: 2}}
+                                type="submit"
+                                variant="contained"
+                                fullWidth
+                            >
+                                Register
+                            </Button>
+                        </Fragment>
+                    </form>
+
+                    <Box sx={{mb: 1}} />
+                </DialogContent>
+            </Dialog>
+        </Fragment>
+    )
 }
+
+export default RegisterDialog
diff --git a/frontend/src/features/Auth/userThunks.ts b/frontend/src/features/Auth/userThunks.ts
index 6bbf7d3..161a00c 100644
--- a/frontend/src/features/Auth/userThunks.ts
+++ b/frontend/src/features/Auth/userThunks.ts
@@ -6,12 +6,29 @@ import jwt from 'jwt-decode'
 const loginError =
     'Server error occurred while logging in. Please contact help service to resolve this issue or try again later.'
 
+const registerError =
+    'Server error occurred while registering. Please contact help service to resolve this issue or try again later.'
+
 // This is not present in the swagger since spring generates
 export interface UserLogin {
     username: string,
     password: string
 }
 
+export interface UserRegister {
+    username: string,
+    passwords: {
+        password: string,
+        confirmationPassword: string,
+    },
+    permissions: {
+        canRead: boolean,
+        canWrite: boolean,
+        canDelete: boolean,
+    },
+    email: string,
+}
+
 export const logIn = createAsyncThunk(
     'user/login',
     async (userDto: UserLogin, { getState }) => {
@@ -50,3 +67,21 @@ export const logIn = createAsyncThunk(
         }
     }
 )
+
+export const register = createAsyncThunk(
+    'user/register',
+    async (userDto: UserRegister) => {
+        try {
+            console.log(userDto.passwords.password)
+            console.log(userDto.passwords.confirmationPassword)
+            const { data, status } = await axiosInstance.post('/users', userDto)
+            if (status !== 200) {
+                // TODO read API err
+                return Promise.reject(loginError)
+            }
+            return status
+        } catch (err: any) {
+            return Promise.reject(loginError)
+        }
+    }
+)
diff --git a/frontend/src/features/Home/EditHome.tsx b/frontend/src/features/Home/EditHome.tsx
index 980afb9..618b3ce 100644
--- a/frontend/src/features/Home/EditHome.tsx
+++ b/frontend/src/features/Home/EditHome.tsx
@@ -23,7 +23,6 @@ const EditHome = () => {
     const navigate = useNavigate()
 
     const updateContent = (updated: string) => {
-        console.log("updateContent: " + updated)
         if (content) {
             content["content"] = updated
         }
@@ -84,8 +83,8 @@ const EditHome = () => {
     {!isContentLoading && content ? (
         <div>
             <h1>Home</h1>
-        <form onSubmit={handleSubmit}>
-    <input type="submit" value="Submit" />
+        <form>
+            <Button type="button" onClick={handleSubmit}>Submit</Button>
     <ReactQuill
         theme='snow'
         value={content["content"]}
diff --git a/frontend/src/features/Home/Home.tsx b/frontend/src/features/Home/Home.tsx
index 2b634b5..db43d40 100644
--- a/frontend/src/features/Home/Home.tsx
+++ b/frontend/src/features/Home/Home.tsx
@@ -1,4 +1,4 @@
-import {Button, Paper} from '@mui/material'
+import {Button, Container, Paper} from '@mui/material'
 import {Fragment, useEffect, useState} from 'react'
 import { useDispatch, useSelector } from 'react-redux'
 import { logout } from '../Auth/userSlice'
@@ -28,16 +28,12 @@ const Home = () => {
         navigate(path)
     }
 
-    const roles = useSelector(
-        (state: RootState) => state.user.roles
+    const isAdmin = useSelector(
+        (state: RootState) => state.user.roles.includes("ROLE_ADMIN")
     )
-
-    const isAdmin = roles.includes("ROLE_ADMIN")
-
     // Fetch the item from the api after mounting the component
     useEffect(() => {
         // Function to fetch the item from the api
-        console.log("useEffect called")
         const fetchItem = async () => {
             try {
                 const { data, status } = await axiosInstance.get(
@@ -55,30 +51,32 @@ const Home = () => {
             }
         }
         fetchItem()
-    }, [roles])
+    }, [])
 
     return (
         <Fragment>
-            <Paper style={{ minHeight: '100vh' }} variant="outlined">
+            <Paper style={{ minHeight: '80vh', display:'flex', justifyContent: 'space-between', flexDirection:'column'}} variant="outlined">
                 {isContentLoading && !err ? <ContentLoading /> : null}
                 {!isContentLoading && content ? (
-                    <div>
+                    <Container>
                         <h1>Home</h1>
                         {formatHtmlStringToReactDom(content["content"] as string)}
-                    </div>
+                    </Container>
                 ) : null}
-                {isAdmin ? (
-                        <Button
-                            startIcon={<EditIcon />}
-                            variant="contained"
-                            component={RouterLink}
-                            to="/editHome"
-                            color="primary"
-                            sx={{ mb: 2 }}
-                        >
-                            Edit
-                        </Button>
 
+                {isAdmin ? (
+                    <Container sx={{ mb: 3 }} >
+                            <Button
+                                startIcon={<EditIcon />}
+                                variant="contained"
+                                component={RouterLink}
+                                to="/editHome"
+                                color="primary"
+                                sx={{ mb: 2 }}
+                            >
+                                Edit
+                            </Button>
+                    </Container>
                 ) : null}
             </Paper>
         </Fragment>
diff --git a/frontend/src/features/Navigation/navigationMenuItems.ts b/frontend/src/features/Navigation/navigationMenuItems.ts
index 6b1aef2..3da5a87 100644
--- a/frontend/src/features/Navigation/navigationMenuItems.ts
+++ b/frontend/src/features/Navigation/navigationMenuItems.ts
@@ -8,6 +8,7 @@ import LoginIcon from '@mui/icons-material/Login'
 import { SvgIconTypeMap } from '@mui/material'
 import DataSaverOffIcon from '@mui/icons-material/DataSaverOff'
 import LogoutIcon from '@mui/icons-material/Logout'
+import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount'
 
 export interface NavigationMenuItem {
     name: string
@@ -79,6 +80,13 @@ const items: NavigationMenuItem[] = [
         icon: DataSaverOffIcon,
         position: 4,
     },
+    {
+        name: 'Administration',
+        path: '/administration',
+        accessibleTo: loggedInAccess,
+        icon: SupervisorAccountIcon,
+        position: 6,
+    },
     {
         name: 'Logout',
         path: '/logout',
diff --git a/frontend/src/features/NotAuthorized/NotAuthorized.tsx b/frontend/src/features/NotAuthorized/NotAuthorized.tsx
new file mode 100644
index 0000000..bb4f885
--- /dev/null
+++ b/frontend/src/features/NotAuthorized/NotAuthorized.tsx
@@ -0,0 +1,14 @@
+import { Button, Stack } from "@mui/material"
+import { Link as RouterLink } from 'react-router-dom'
+
+const NotAuthorized = () => {
+
+    return (
+        <Stack alignItems={"center"} sx={{my: 4}}>
+            <h1>Not Authorized 😔</h1>
+            <Button variant="outlined" component={RouterLink} to="/" >Return Home</Button>
+        </Stack>
+    )
+}
+
+export default NotAuthorized
\ No newline at end of file
diff --git a/frontend/src/features/Theme/ThemeWrapper.tsx b/frontend/src/features/Theme/ThemeWrapper.tsx
index 93e10c0..ee24e83 100644
--- a/frontend/src/features/Theme/ThemeWrapper.tsx
+++ b/frontend/src/features/Theme/ThemeWrapper.tsx
@@ -16,6 +16,9 @@ const ThemeWrapper: FunctionComponent<ThemeWrapperProps> = ({ children }) => {
         secondary: {
             main: '#E3DBCF',
         },
+        selected: {
+            main: '#C0C0C0'
+        }
     }
 
     const darkThemePalette = {
