diff --git a/client/src/actions/roles.ts b/client/src/actions/roles.ts new file mode 100644 index 0000000000000000000000000000000000000000..0160353b184755b33f003592ad127880af2803d7 --- /dev/null +++ b/client/src/actions/roles.ts @@ -0,0 +1,15 @@ +import axios from 'axios' +import { AppDispatch } from './../store' +import Types from './types' + +export const getRoles = () => async (dispatch: AppDispatch) => { + await axios + .get('/misc/roles') + .then((res) => { + dispatch({ + type: Types.SET_ROLES, + payload: res.data.items, + }) + }) + .catch((err) => console.log(err)) +} diff --git a/client/src/actions/searchUser.ts b/client/src/actions/searchUser.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb4bf122e40d13952537c3e942bf351f10f99604 --- /dev/null +++ b/client/src/actions/searchUser.ts @@ -0,0 +1,39 @@ +import axios from 'axios' +import { UserFilterParams } from '../interfaces/UserData' +import { AppDispatch, RootState } from './../store' +import Types from './types' + +export const getSearchUsers = () => async (dispatch: AppDispatch, getState: () => RootState) => { + const currentParams: UserFilterParams = getState().searchUsers.filterParams + // Send params in snake-case for api + const params = { + page_size: currentParams.pageSize, + role_id: currentParams.roleId, + city_id: currentParams.cityId, + name: currentParams.name, + page: currentParams.page, + email: currentParams.email, + } + await axios + .get('/users/search', { params }) + .then((res) => { + dispatch({ + type: Types.SET_SEARCH_USERS, + payload: res.data.items, + }) + dispatch({ + type: Types.SET_SEARCH_USERS_TOTAL_COUNT, + payload: res.data.total_count, + }) + dispatch({ + type: Types.SET_SEARCH_USERS_COUNT, + payload: res.data.count, + }) + }) + .catch((err) => { + console.log(err) + }) +} +export const setFilterParams = (params: UserFilterParams) => (dispatch: AppDispatch) => { + dispatch({ type: Types.SET_SEARCH_USERS_FILTER_PARAMS, payload: params }) +} diff --git a/client/src/actions/types.ts b/client/src/actions/types.ts index 40e9430369e7314b77e7dcc133132e98de1f67f6..b72a35862f4c42161a25997a6065662d1020e8f1 100644 --- a/client/src/actions/types.ts +++ b/client/src/actions/types.ts @@ -1,7 +1,12 @@ export default { LOADING_UI: 'LOADING_UI', LOADING_USER: 'LOADING_USER', + SET_ROLES: 'SET_ROLES', SET_USER: 'SET_USER', + SET_SEARCH_USERS: 'SET_SEARCH_USERS', + SET_SEARCH_USERS_FILTER_PARAMS: 'SET_SEARCH_USERS_FILTER_PARAMS', + SET_SEARCH_USERS_COUNT: 'SET_SEARCH_USERS_COUNT', + SET_SEARCH_USERS_TOTAL_COUNT: 'SET_SEARCH_USERS_TOTAL_COUNT', SET_ERRORS: 'SET_ERRORS', CLEAR_ERRORS: 'SET_ERRORS', SET_UNAUTHENTICATED: 'SET_UNAUTHENTICATED', diff --git a/client/src/interfaces/Role.ts b/client/src/interfaces/Role.ts index 4165fe7beddd0d6787ccf67aabc5409dd8612623..08a54dfcb012ee03398d17d4a7153ab040cde909 100644 --- a/client/src/interfaces/Role.ts +++ b/client/src/interfaces/Role.ts @@ -1,9 +1,4 @@ -import { City } from './City' - -export interface Competition { - name: string - city: City - style_id: number - year: number +export interface Role { id: number + name: string } diff --git a/client/src/interfaces/SearchUserFilterParams.ts b/client/src/interfaces/SearchUserFilterParams.ts new file mode 100644 index 0000000000000000000000000000000000000000..8d7230d83d12d164a0db4683b4db7b791d835a81 --- /dev/null +++ b/client/src/interfaces/SearchUserFilterParams.ts @@ -0,0 +1,8 @@ +export interface SearchUSerFilterParams { + name?: string + year?: number + cityId?: number + styleId?: number + page: number + pageSize: number +} diff --git a/client/src/interfaces/UserData.ts b/client/src/interfaces/UserData.ts new file mode 100644 index 0000000000000000000000000000000000000000..a0c7394699b314c49e8f19bb259b6d73dd610bcf --- /dev/null +++ b/client/src/interfaces/UserData.ts @@ -0,0 +1,16 @@ +export interface UserData { + id: number + name?: string + email: string + role_id: number + city_id: number +} + +export interface UserFilterParams { + name?: string + email?: string + cityId?: number + roleId?: number + page: number + pageSize: number +} diff --git a/client/src/interfaces/models.ts b/client/src/interfaces/models.ts index 91920bd635e522072ff206b8e03554bb788c969f..70d4cc08b5f8e449ad251526389dd84fe42a4d3e 100644 --- a/client/src/interfaces/models.ts +++ b/client/src/interfaces/models.ts @@ -12,3 +12,18 @@ export interface AddCompetitionModel { export interface CompetitionLoginModel { code: string } + +export interface AddUserModel { + email: string + password: string + role: string + city: string + name?: string +} + +export interface EditUserModel { + email: string + role: string + city: string + name?: string +} diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx index 3d2b94b87d67a9d325ee18d42e9f8d4aa80a9978..270e2dd0c8c3ec20a57fbe628605b8ecf574d205 100644 --- a/client/src/pages/admin/AdminPage.tsx +++ b/client/src/pages/admin/AdminPage.tsx @@ -19,6 +19,7 @@ import { logoutUser } from '../../actions/user' import { useAppDispatch } from '../../hooks' import CompetitionManager from './components/CompetitionManager' import Regions from './components/Regions' +import UserManager from './components/UserManager' import { LeftDrawer } from './styled' const drawerWidth = 250 @@ -112,9 +113,7 @@ const AdminView: React.FC = () => { <Regions /> </Route> <Route path={`${path}/användare`}> - <Typography variant="h1" noWrap> - Användare - </Typography> + <UserManager /> </Route> <Route path={`${path}/tävlingshanterare`}> <CompetitionManager /> diff --git a/client/src/pages/admin/components/AddUser.tsx b/client/src/pages/admin/components/AddUser.tsx new file mode 100644 index 0000000000000000000000000000000000000000..81c6adc96b26e230acf1ee2b73b68b28c293d7be --- /dev/null +++ b/client/src/pages/admin/components/AddUser.tsx @@ -0,0 +1,220 @@ +import { Button, FormControl, InputLabel, MenuItem, Popover, TextField } from '@material-ui/core' +import { Alert, AlertTitle } from '@material-ui/lab' +import axios from 'axios' +import { Formik, FormikHelpers } from 'formik' +import React from 'react' +import * as Yup from 'yup' +import { getSearchUsers } from '../../../actions/searchUser' +import { useAppDispatch, useAppSelector } from '../../../hooks' +import { City } from '../../../interfaces/City' +import { AddUserModel } from '../../../interfaces/models' +import { Role } from '../../../interfaces/Role' +import { AddCompetitionButton, AddCompetitionContent, AddCompetitionForm } from './styled' + +interface ServerResponse { + code: number + message: string +} + +interface AddUserFormModel { + model: AddUserModel + error?: string +} + +const noRoleSelected = 'Välj roll' +const noCitySelected = 'Välj stad' + +const userSchema: Yup.SchemaOf<AddUserFormModel> = Yup.object({ + model: Yup.object() + .shape({ + name: Yup.string(), //.required('Namn krävs'), + email: Yup.string().email().required('Email krävs'), + password: Yup.string() + .required('Lösenord krävs.') + .min(6, 'Lösenord måste vara minst 6 tecken.') + .matches(/[a-zA-Z]/, 'Lösenord får enbart innehålla a-z, A-Z.'), + role: Yup.string().required('Roll krävs').notOneOf([noCitySelected], 'Välj en roll'), + city: Yup.string().required('Stad krävs').notOneOf([noRoleSelected], 'Välj en stad'), + }) + .required(), + error: Yup.string().optional(), +}) + +const AddUser: React.FC = (props: any) => { + const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null) + const [selectedRole, setSelectedRole] = React.useState<Role | undefined>() + const roles = useAppSelector((state) => state.roles.roles) + + const [selectedCity, setSelectedCity] = React.useState<City | undefined>() + const cities = useAppSelector((state) => state.cities.cities) + + const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { + setAnchorEl(event.currentTarget) + } + const handleClose = () => { + setAnchorEl(null) + } + + const open = Boolean(anchorEl) + const dispatch = useAppDispatch() + const id = open ? 'simple-popover' : undefined + const handleCompetitionSubmit = async (values: AddUserFormModel, actions: FormikHelpers<AddUserFormModel>) => { + const params = { + email: values.model.email, + password: values.model.password, + //name: values.model.name, + city_id: selectedCity?.id as number, + role_id: selectedRole?.id as number, + } + await axios + .post<ServerResponse>('/auth/signup', params) + .then(() => { + actions.resetForm() + setAnchorEl(null) + dispatch(getSearchUsers()) + setSelectedCity(undefined) + setSelectedRole(undefined) + }) + .catch(({ response }) => { + console.warn(response.data) + if (response.data && response.data.message) + actions.setFieldError('error', response.data && response.data.message) + else actions.setFieldError('error', 'Something went wrong, please try again') + }) + .finally(() => { + actions.setSubmitting(false) + }) + } + + const userInitialValues: AddUserFormModel = { + model: { email: '', password: '', name: '', city: noCitySelected, role: noRoleSelected }, + } + return ( + <div> + <AddCompetitionButton color="secondary" variant="contained" onClick={handleClick}> + Ny Användare + </AddCompetitionButton> + <Popover + id={id} + open={open} + anchorEl={anchorEl} + onClose={handleClose} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'center', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'center', + }} + > + <AddCompetitionContent> + <Formik initialValues={userInitialValues} validationSchema={userSchema} onSubmit={handleCompetitionSubmit}> + {(formik) => ( + <AddCompetitionForm onSubmit={formik.handleSubmit}> + <TextField + label="Email" + name="model.email" + helperText={formik.touched.model?.email ? formik.errors.model?.email : ''} + error={Boolean(formik.touched.model?.email && formik.errors.model?.email)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <TextField + label="Lösenord" + name="model.password" + helperText={formik.touched.model?.password ? formik.errors.model?.password : ''} + error={Boolean(formik.touched.model?.password && formik.errors.model?.password)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <TextField + label="Namn" + name="model.name" + helperText={formik.touched.model?.name ? formik.errors.model?.name : ''} + error={Boolean(formik.touched.model?.name && formik.errors.model?.name)} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + margin="normal" + /> + <FormControl> + <InputLabel shrink id="demo-customized-select-native"> + Region + </InputLabel> + <TextField + select + name="model.city" + id="standard-select-currency" + value={selectedCity ? selectedCity.name : noCitySelected} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + error={Boolean(formik.errors.model?.city && formik.touched.model?.city)} + helperText={formik.touched.model?.city && formik.errors.model?.city} + margin="normal" + > + <MenuItem value={noCitySelected} onClick={() => setSelectedCity(undefined)}> + {noCitySelected} + </MenuItem> + {cities && + cities.map((city) => ( + <MenuItem key={city.name} value={city.name} onClick={() => setSelectedCity(city)}> + {city.name} + </MenuItem> + ))} + </TextField> + </FormControl> + + <FormControl> + <InputLabel shrink id="demo-customized-select-native"> + Roll + </InputLabel> + <TextField + select + name="model.role" + id="standard-select-currency" + value={selectedRole ? selectedRole.name : noRoleSelected} + onChange={formik.handleChange} + onBlur={formik.handleBlur} + error={Boolean(formik.errors.model?.role && formik.touched.model?.role)} + helperText={formik.touched.model?.role && formik.errors.model?.role} + margin="normal" + > + <MenuItem value={noRoleSelected} onClick={() => setSelectedRole(undefined)}> + {noRoleSelected} + </MenuItem> + {roles && + roles.map((role) => ( + <MenuItem key={role.name} value={role.name} onClick={() => setSelectedRole(role)}> + {role.name} + </MenuItem> + ))} + </TextField> + </FormControl> + + <Button + type="submit" + fullWidth + variant="contained" + color="secondary" + disabled={!formik.isValid || !formik.values.model?.name || !formik.values.model?.city} + > + Lägg till + </Button> + {formik.errors.error && ( + <Alert severity="error"> + <AlertTitle>Error</AlertTitle> + {formik.errors.error} + </Alert> + )} + </AddCompetitionForm> + )} + </Formik> + </AddCompetitionContent> + </Popover> + </div> + ) +} + +export default AddUser diff --git a/client/src/pages/admin/components/Regions.test.tsx b/client/src/pages/admin/components/Regions.test.tsx deleted file mode 100644 index 6f7fb873cba29a4a56f736b0ee7d7789ba420428..0000000000000000000000000000000000000000 --- a/client/src/pages/admin/components/Regions.test.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { render } from '@testing-library/react' -import React from 'react' -import Regions from './Regions' - -it('renders regions', () => { - render(<Regions />) -}) diff --git a/client/src/pages/admin/components/Regions.tsx b/client/src/pages/admin/components/Regions.tsx index d58802d06e8c53261a10e183c31984ca767e2f99..103c2b1d820643a3fe80b2a2e241e42505eb6e5d 100644 --- a/client/src/pages/admin/components/Regions.tsx +++ b/client/src/pages/admin/components/Regions.tsx @@ -1,12 +1,122 @@ -import Typography from '@material-ui/core/Typography' -import React from 'react' +import { Button, Menu, TextField, Typography } from '@material-ui/core' +import FormControl from '@material-ui/core/FormControl' +import Paper from '@material-ui/core/Paper' +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' +import Table from '@material-ui/core/Table' +import TableBody from '@material-ui/core/TableBody' +import TableCell from '@material-ui/core/TableCell' +import TableContainer from '@material-ui/core/TableContainer' +import TableHead from '@material-ui/core/TableHead' +import TableRow from '@material-ui/core/TableRow' +import MoreHorizIcon from '@material-ui/icons/MoreHoriz' +import axios from 'axios' +import React, { useEffect } from 'react' +import { getCities } from '../../../actions/cities' +import { useAppDispatch, useAppSelector } from '../../../hooks' +import { RemoveCompetition, TopBar } from './styled' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + table: { + width: '100%', + }, + margin: { + margin: theme.spacing(1), + }, + }) +) + +const UserManager: React.FC = (props: any) => { + const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) + const [activeId, setActiveId] = React.useState<number | undefined>(undefined) + const citiesTotal = useAppSelector((state) => state.cities.total) + const cities = useAppSelector((state) => state.cities.cities) + const [newCity, setNewCity] = React.useState() + const classes = useStyles() + const dispatch = useAppDispatch() + const handleClose = () => { + setAnchorEl(null) + setActiveId(undefined) + } + + useEffect(() => { + dispatch(getCities()) + }, []) + + const handleDeleteCity = async () => { + if (activeId) { + await axios + .delete(`/misc/cities/${activeId}`) + .then(() => { + setAnchorEl(null) + dispatch(getCities()) + }) + .catch(({ response }) => { + console.warn(response.data) + }) + } + } + + const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => { + setAnchorEl(event.currentTarget) + setActiveId(id) + } + + const handleAddCity = async () => { + await axios + .post(`/misc/cities`, { name: newCity }) + .then(() => { + setAnchorEl(null) + dispatch(getCities()) + }) + .catch(({ response }) => { + console.warn(response.data) + }) + } + + const handleChange = (event: any) => { + setNewCity(event.target.value) + } -const Regions: React.FC = () => { return ( - <Typography variant="h1" noWrap> - Regions - </Typography> + <div> + <TopBar> + <FormControl className={classes.margin}> + <TextField className={classes.margin} value={newCity} onChange={handleChange} label="Region"></TextField> + <Button color="primary" variant="contained" onClick={handleAddCity}> + Lägg till + </Button> + </FormControl> + </TopBar> + <TableContainer component={Paper}> + <Table className={classes.table} aria-label="simple table"> + <TableHead> + <TableRow> + <TableCell>Regioner</TableCell> + <TableCell align="right"></TableCell> + </TableRow> + </TableHead> + <TableBody> + {cities && + cities.map((row) => ( + <TableRow key={row.name}> + <TableCell scope="row">{row.name}</TableCell> + <TableCell align="right"> + <Button onClick={(event) => handleClick(event, row.id)}> + <MoreHorizIcon /> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + {(!cities || cities.length === 0) && <Typography>Inga regioner hittades</Typography>} + </TableContainer> + <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> + <RemoveCompetition onClick={handleDeleteCity}>Ta bort</RemoveCompetition> + </Menu> + </div> ) } -export default Regions +export default UserManager diff --git a/client/src/pages/admin/components/UserManager.tsx b/client/src/pages/admin/components/UserManager.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bc3d55d480a38cc4d0c92a56d84b3e2d7ae2bb18 --- /dev/null +++ b/client/src/pages/admin/components/UserManager.tsx @@ -0,0 +1,201 @@ +import { Button, Menu, TablePagination, TextField, Typography } from '@material-ui/core' +import FormControl from '@material-ui/core/FormControl' +import InputLabel from '@material-ui/core/InputLabel' +import MenuItem from '@material-ui/core/MenuItem' +import Paper from '@material-ui/core/Paper' +import Select from '@material-ui/core/Select' +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles' +import Table from '@material-ui/core/Table' +import TableBody from '@material-ui/core/TableBody' +import TableCell from '@material-ui/core/TableCell' +import TableContainer from '@material-ui/core/TableContainer' +import TableHead from '@material-ui/core/TableHead' +import TableRow from '@material-ui/core/TableRow' +import MoreHorizIcon from '@material-ui/icons/MoreHoriz' +import axios from 'axios' +import React, { useEffect } from 'react' +import { getCities } from '../../../actions/cities' +import { getRoles } from '../../../actions/roles' +import { getSearchUsers, setFilterParams } from '../../../actions/searchUser' +import { useAppDispatch, useAppSelector } from '../../../hooks' +import { UserFilterParams } from '../../../interfaces/UserData' +import AddUser from './AddUser' +import { FilterContainer, RemoveCompetition, TopBar } from './styled' + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + table: { + width: '100%', + }, + margin: { + margin: theme.spacing(1), + }, + }) +) + +const UserManager: React.FC = (props: any) => { + const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null) + const [activeId, setActiveId] = React.useState<number | undefined>(undefined) + const [timerHandle, setTimerHandle] = React.useState<number | undefined>(undefined) + const users = useAppSelector((state) => state.searchUsers.users) + const filterParams = useAppSelector((state) => state.searchUsers.filterParams) + const usersTotal = useAppSelector((state) => state.searchUsers.total) + const cities = useAppSelector((state) => state.cities.cities) + const roles = useAppSelector((state) => state.roles.roles) + const classes = useStyles() + const noFilterText = 'Alla' + const dispatch = useAppDispatch() + const handleClick = (event: React.MouseEvent<HTMLButtonElement>, id: number) => { + setAnchorEl(event.currentTarget) + setActiveId(id) + } + + const handleClose = () => { + setAnchorEl(null) + setActiveId(undefined) + } + + useEffect(() => { + dispatch(getCities()) + dispatch(getRoles()) + dispatch(getSearchUsers()) + }, []) + + const onSearchChange = (event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + if (timerHandle) { + clearTimeout(timerHandle) + setTimerHandle(undefined) + } + //Only updates filter and api 100ms after last input was made + setTimerHandle(window.setTimeout(() => dispatch(getSearchUsers()), 100)) + dispatch(setFilterParams({ ...filterParams, email: event.target.value })) + } + + const handleDeleteUsers = async () => { + if (activeId) { + await axios + .delete(`/auth/delete/${activeId}`) + .then(() => { + setAnchorEl(null) + dispatch(getSearchUsers()) + }) + .catch(({ response }) => { + console.warn(response.data) + }) + } + } + + const handleFilterChange = (newParams: UserFilterParams) => { + dispatch(setFilterParams(newParams)) + dispatch(getSearchUsers()) + } + + return ( + <div> + <TopBar> + <FilterContainer> + <TextField + className={classes.margin} + value={filterParams.email || ''} + onChange={onSearchChange} + label="Sök" + ></TextField> + <FormControl className={classes.margin}> + <InputLabel shrink id="demo-customized-select-native"> + Region + </InputLabel> + <Select + labelId="demo-customized-select-label" + id="demo-customized-select" + value={filterParams.cityId ? cities.find((city) => filterParams.cityId === city.id)?.name : noFilterText} + > + <MenuItem value={noFilterText} onClick={() => handleFilterChange({ ...filterParams, cityId: undefined })}> + {noFilterText} + </MenuItem> + {cities && + cities.map((city) => ( + <MenuItem + key={city.name} + value={city.name} + onClick={() => handleFilterChange({ ...filterParams, cityId: city.id })} + > + {city.name} + </MenuItem> + ))} + </Select> + </FormControl> + <FormControl className={classes.margin}> + <InputLabel shrink id="demo-customized-select-native"> + Roles + </InputLabel> + <Select + labelId="demo-customized-select-label" + id="demo-customized-select" + value={filterParams.roleId ? roles.find((role) => filterParams.roleId === role.id)?.name : noFilterText} + > + <MenuItem value={noFilterText} onClick={() => handleFilterChange({ ...filterParams, roleId: undefined })}> + {noFilterText} + </MenuItem> + {cities && + cities.map((role) => ( + <MenuItem + key={role.name} + value={role.name} + onClick={() => handleFilterChange({ ...filterParams, roleId: role.id })} + > + {role.name} + </MenuItem> + ))} + </Select> + </FormControl> + </FilterContainer> + <AddUser /> + </TopBar> + <TableContainer component={Paper}> + <Table className={classes.table} aria-label="simple table"> + <TableHead> + <TableRow> + <TableCell>Email</TableCell> + <TableCell>Namn</TableCell> + <TableCell>Region</TableCell> + <TableCell>Roll</TableCell> + <TableCell align="right"></TableCell> + </TableRow> + </TableHead> + <TableBody> + {users && + users.map((row) => ( + <TableRow key={row.email}> + <TableCell scope="row">{row.email}</TableCell> + <TableCell scope="row">{row.name}</TableCell> + <TableCell>{cities.find((city) => city.id === row.city_id)?.name || ''}</TableCell> + <TableCell>{roles.find((role) => role.id === row.role_id)?.name || ''}</TableCell> + <TableCell align="right"> + <Button onClick={(event) => handleClick(event, row.id)}> + <MoreHorizIcon /> + </Button> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + {(!users || users.length === 0) && <Typography>Inga tävlingar hittades med nuvarande filter</Typography>} + </TableContainer> + <TablePagination + component="div" + rowsPerPageOptions={[]} + rowsPerPage={filterParams.pageSize} + count={usersTotal} + page={filterParams.page} + onChangePage={(event, newPage) => handleFilterChange({ ...filterParams, page: newPage })} + /> + <Menu id="simple-menu" anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={handleClose}> + <MenuItem>Redigera</MenuItem> + <MenuItem>Byt lösenord</MenuItem> + <RemoveCompetition onClick={handleDeleteUsers}>Ta bort</RemoveCompetition> + </Menu> + </div> + ) +} + +export default UserManager diff --git a/client/src/reducers/allReducers.ts b/client/src/reducers/allReducers.ts index b12b534633d0b7d71eab7f82533bdd2d0096a3a0..3c9f2b6cecf5166e9c21c8366b97345a2e2827ec 100644 --- a/client/src/reducers/allReducers.ts +++ b/client/src/reducers/allReducers.ts @@ -3,6 +3,8 @@ import { combineReducers } from 'redux' import citiesReducer from './citiesReducer' import competitionsReducer from './competitionsReducer' +import rolesReducer from './rolesReducer' +import searchUserReducer from './searchUserReducer' import uiReducer from './uiReducer' import userReducer from './userReducer' @@ -12,5 +14,7 @@ const allReducers = combineReducers({ UI: uiReducer, competitions: competitionsReducer, cities: citiesReducer, + roles: rolesReducer, + searchUsers: searchUserReducer, }) export default allReducers diff --git a/client/src/reducers/rolesReducer.ts b/client/src/reducers/rolesReducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..652d4afcf12c1f58aad0c1b9cf21ab36e2ca96da --- /dev/null +++ b/client/src/reducers/rolesReducer.ts @@ -0,0 +1,19 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' +import { Role } from '../interfaces/Role' + +interface RoleState { + roles: Role[] +} +const initialState: RoleState = { + roles: [], +} + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_ROLES: + return { ...state, roles: action.payload as Role[] } + default: + return state + } +} diff --git a/client/src/reducers/searchUserReducer.ts b/client/src/reducers/searchUserReducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..9b77d68af26b3fbf2f0b428c8a6a959f6e9c5991 --- /dev/null +++ b/client/src/reducers/searchUserReducer.ts @@ -0,0 +1,44 @@ +import { AnyAction } from 'redux' +import Types from '../actions/types' +import { UserData, UserFilterParams } from '../interfaces/UserData' + +interface SearchUserState { + users: UserData[] + total: number + count: number + filterParams: UserFilterParams +} + +const initialState: SearchUserState = { + users: [], + total: 0, + count: 0, + filterParams: { pageSize: 10, page: 0 }, +} + +export default function (state = initialState, action: AnyAction) { + switch (action.type) { + case Types.SET_SEARCH_USERS: + return { + ...state, + users: action.payload as UserData[], + } + case Types.SET_SEARCH_USERS_FILTER_PARAMS: + return { + ...state, + filterParams: action.payload as UserFilterParams, + } + case Types.SET_SEARCH_USERS_TOTAL_COUNT: + return { + ...state, + total: action.payload as number, + } + case Types.SET_SEARCH_USERS_COUNT: + return { + ...state, + count: action.payload as number, + } + default: + return state + } +}