diff --git a/src/pages/scenarios/tables/results-table.js b/src/pages/scenarios/tables/results-table.js index f0ff497..aca569c 100644 --- a/src/pages/scenarios/tables/results-table.js +++ b/src/pages/scenarios/tables/results-table.js @@ -27,6 +27,7 @@ import {Button} from "react-bootstrap"; import NotificationsFactory from "../../../common/data-managers/notifications-factory"; import notificationsDataManager from "../../../common/data-managers/notifications-data-manager"; import FileSaver from "file-saver"; +import moment from "moment"; import { useGetResultsQuery, useAddResultMutation, @@ -142,6 +143,11 @@ const ResultsTable = (props) => { setIsDeleteModalOpened(false); setResultToDelete({}); } + + const stateUpdateModifier = (dateString) => { + const date = moment(dateString); + return `${date.fromNow()}`; + }; return (
@@ -175,11 +181,13 @@ const ResultsTable = (props) => { stateUpdateModifier(createdAt)} width={200} /> stateUpdateModifier(updatedAt)} width={200} /> . + ******************************************************************************/ + +import React from 'react'; +import { useState } from 'react'; +import { Form, Col, Button} from 'react-bootstrap'; +import Dialog from '../../../common/dialogs/dialog'; +import { useGetScenariosQuery } from '../../../store/apiSlice'; + +const AddScenarioMappingDialog = ({isDialogOpened, onClose, mappings}) => { + + const [name, setName] = useState(''); + const [isValid, setIsValid] = useState(false); + const [selectedOption, setSelectedOption] = useState('addUsersToScenario'); + const [selectedScenarioID, setSelectedScenarioID] = useState(''); + + const {data: {scenarios} = {}, isLoading: isLoadingScenarios} = useGetScenariosQuery(); + + const handleRadioChange = (e) => { + setSelectedOption(e.target.value); + } + + const handleClose = (canceled) => { + if(canceled) { + onClose(null); + } else { + onClose({scenarioID: Number(selectedScenarioID), duplicate: selectedOption === 'duplicateScenarioForUsers'}); + } + } + + const handleSelectChange = (e) => { + setSelectedScenarioID(e.target.value); + setIsValid(e.target.value !== ''); + }; + + return ( {}} + valid={isValid}> +
+ +
+ + +
+
+ + + Select Option + {isLoadingScenarios ?
Loading...
: ( + + + {scenarios.map(scenario => { + //check if existing mappings are already added to the usergroup + if(!mappings.some(mapping => mapping.scenarioID === scenario.id)) return ; + })} + + )} +
+
+
); +} + +export default AddScenarioMappingDialog; diff --git a/src/pages/usergroups/dialogs/addUserToUsergroupDialog.js b/src/pages/usergroups/dialogs/addUserToUsergroupDialog.js new file mode 100644 index 0000000..64dab95 --- /dev/null +++ b/src/pages/usergroups/dialogs/addUserToUsergroupDialog.js @@ -0,0 +1,81 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import { useState, useEffect } from 'react'; +import { Form, Col, Dropdown, Badge } from 'react-bootstrap'; +import Dialog from '../../../common/dialogs/dialog'; +import { useGetUsersQuery } from '../../../store/apiSlice'; + +const AddUserToUsergroupDialog = ({isModalOpened, onClose, currentUsers}) => { + + const [selectedUsers, setSelectedusers] = useState([]); + const [isValid, setIsValid] = useState(false); + + const {data: {users} = {}, isLoading: isLoadingUsers} = useGetUsersQuery(); + + const toggleUser = (event, option) => { + event.preventDefault(); + if (selectedUsers.includes(option)) { + setSelectedusers(prevState => ([...prevState.filter((item) => item !== option)])); + } else { + setSelectedusers(prevState => ([...prevState, option])); + } + } + + useEffect(() => { + setIsValid(selectedUsers.length > 0); + }, [selectedUsers]); + + const handleClose = (canceled) => { + if(!canceled){ + onClose(selectedUsers); + } else { + onClose([]); + } + } + + const handleReset = () => { + setSelectedusers([]); + } + + return ( + + + Select Options + + {isLoadingUsers ?
Loading...
: [...users].filter(user => !currentUsers.some(currentUser => currentUser.id == user.id)).map(user => + toggleUser(event, user)} + style={{ + backgroundColor: selectedUsers.includes(user) ? '#d3d3d3' : '' + }} + > + {user.username} + )} +
+
+
+ {selectedUsers.length > 0 ? selectedUsers.map(user => + {user.username}) :
No users selected
} +
+
+ ); +} + +export default AddUserToUsergroupDialog; diff --git a/src/pages/usergroups/dialogs/renameGroupDialog.js b/src/pages/usergroups/dialogs/renameGroupDialog.js new file mode 100644 index 0000000..cfab466 --- /dev/null +++ b/src/pages/usergroups/dialogs/renameGroupDialog.js @@ -0,0 +1,69 @@ +/** + * This file is part of VILLASweb. + * + * VILLASweb is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * VILLASweb is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with VILLASweb. If not, see . + ******************************************************************************/ + +import React from 'react'; +import { useState, useEffect } from 'react'; +import { Form, Col, Button} from 'react-bootstrap'; +import Dialog from '../../../common/dialogs/dialog'; + +const RenameUsergroupDialog = ({isModalOpened, onClose, oldName}) => { + + const [name, setName] = useState(oldName); + const [isValid, setIsValid] = useState(false); + + useEffect(() => { + if (isModalOpened && oldName) { + setName(oldName); + } + }, [isModalOpened, oldName]); + + const handleNameChange = (e) => { + const newName = e.target.value; + setName(newName); + setIsValid(newName.length >= 3 && !(/^\s/.test(newName))); + } + + const handleClose = (canceled) => { + if(canceled) { + onClose(null); + } else { + onClose(name); + } + } + + const handleReset = () => { + setName(''); + } + + return ( +
+ + Name + + + +
+
); +} + +export default RenameUsergroupDialog; diff --git a/src/pages/usergroups/tables/usergroup-scenarios-table.js b/src/pages/usergroups/tables/usergroup-scenarios-table.js index b6716e2..28e270c 100644 --- a/src/pages/usergroups/tables/usergroup-scenarios-table.js +++ b/src/pages/usergroups/tables/usergroup-scenarios-table.js @@ -15,16 +15,60 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import { useGetUsergroupByIdQuery } from "../../../store/apiSlice"; +import { useState } from "react"; +import { useGetScenariosQuery, useGetUsergroupByIdQuery } from "../../../store/apiSlice"; import { Table, DataColumn, LinkColumn, ButtonColumn } from "../../../common/table"; import { iconStyle, buttonStyle } from "../styles"; import IconButton from "../../../common/buttons/icon-button"; +import AddScenarioMappingDialog from "../dialogs/addScenarioMappingDialog"; +import { useUpdateUsergroupMutation } from "../../../store/apiSlice"; +import DeleteDialog from "../../../common/dialogs/delete-dialog"; const UsergroupScenariosTable = ({usergroupID}) => { - const {data: {usergroup} = {}, isLoading} = useGetUsergroupByIdQuery(usergroupID); + const {data: {usergroup} = {}, isLoading, refetch} = useGetUsergroupByIdQuery(usergroupID); + const {data: {scenarios} = {}, isLoading: isScenariosLoading } = useGetScenariosQuery(); + const [isAddScenarioDialogOpen, setIsAddScenarioDialogOpen] = useState(false); + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); + const [mappingToDelete, setMappingToDelete] = useState({scenarioID: null}); + const [updateUsergroup] = useUpdateUsergroupMutation(); - const handleAddScenarioMapping = () => { + const handleAddScenarioMapping = async (newMapping) => { + if(newMapping){ + try{ + //add new mapping while saving name and existing mappings if there are any + const oldMappings = usergroup.scenarioMappings.length > 0 ? [...usergroup.scenarioMappings] : []; + await updateUsergroup({usergroupID: usergroupID, usergroup: {name: usergroup.name, scenarioMappings: [...oldMappings, newMapping]}}).unwrap(); + refetch(); + } catch(error) { + console.log("Error updating mappings", error); + } + } + setIsAddScenarioDialogOpen(false); + } + const handleRemoveScenarioMapping = async (isConfirmed) => { + if(isConfirmed){ + try { + //update usergroup with new mappings without the target + const newMappings = [...usergroup.scenarioMappings].filter(mapping => mapping.id !== mappingToDelete.id); + await updateUsergroup({usergroupID: usergroupID, usergroup: {name: usergroup.name, scenarioMappings: newMappings}}).unwrap(); + refetch(); + } catch (error) { + console.log("Error removing mapping", error); + } + } + setIsDeleteDialogOpen(false); + setMappingToDelete({scenarioID: null}); + } + + const getScenarioName = (scenarioID) => { + if(isScenariosLoading){ + return
Loading...
; + } + + const scenario = scenarios.find((scenario) => scenario.id === scenarioID); + + return scenario ?
{scenario.name}
:
unknown
; } const getDuplicateLabel = (duplicate) => { @@ -40,7 +84,7 @@ const UsergroupScenariosTable = ({usergroupID}) => { handleAddScenarioMapping()} + onClick={() => setIsAddScenarioDialogOpen(true)} icon="plus" buttonStyle={buttonStyle} iconStyle={iconStyle} @@ -48,14 +92,27 @@ const UsergroupScenariosTable = ({usergroupID}) => { - - - getDuplicateLabel(duplicate)}/> - {/* */} + + getScenarioName(scenarioID)}/> + getDuplicateLabel(duplicate)}/> + { + setMappingToDelete(usergroup.scenarioMappings[index]); + setIsDeleteDialogOpen(true); + }} + />
+ + handleAddScenarioMapping(newMapping)} /> + handleRemoveScenarioMapping(isConfirmed)} + />
); } diff --git a/src/pages/usergroups/tables/usergroup-users-table.js b/src/pages/usergroups/tables/usergroup-users-table.js index 30c1633..dd4a65f 100644 --- a/src/pages/usergroups/tables/usergroup-users-table.js +++ b/src/pages/usergroups/tables/usergroup-users-table.js @@ -15,16 +15,40 @@ * along with VILLASweb. If not, see . ******************************************************************************/ -import { useGetUsersByUsergroupIdQuery } from "../../../store/apiSlice"; +import { useState } from "react"; +import { useGetUsersByUsergroupIdQuery, useAddUserToUsergroupMutation, useDeleteUserFromUsergroupMutation } from "../../../store/apiSlice"; import { Table, DataColumn, LinkColumn, ButtonColumn } from "../../../common/table"; import { iconStyle, buttonStyle } from "../styles"; import IconButton from "../../../common/buttons/icon-button"; +import AddUserToUsergroupDialog from "../dialogs/addUserToUsergroupDialog"; +import NotificationsFactory from "../../../common/data-managers/notifications-factory"; +import notificationsDataManager from "../../../common/data-managers/notifications-data-manager"; const UsergroupUsersTable = ({usergroupID}) => { - const {data: {users}=[], isLoading} = useGetUsersByUsergroupIdQuery(usergroupID); + const {data: {users}=[], isLoading, refetch} = useGetUsersByUsergroupIdQuery(usergroupID); + const [isAddUserDialogOpen, setIsAddUserDialogOpen] = useState(false); + const [addUserToUsergroup] = useAddUserToUsergroupMutation(); + const [removeUserFromGroup] = useDeleteUserFromUsergroupMutation(); - const handleAddUser = () => { + const handleAddUser = async (selectedUsers) => { + if(selectedUsers.length > 0){ + try { + await Promise.all(selectedUsers.map(user => addUserToUsergroup({usergroupID: usergroupID, username: user.username}).unwrap())); + refetch(); + } catch (error) { + console.log('Error adding users', error); + } + } + setIsAddUserDialogOpen(false); + } + const handleRemoveUser = async (user) => { + try{ + await removeUserFromGroup({usergroupID: usergroupID, username: user.username}).unwrap(); + refetch(); + } catch(error) { + console.log('Error removing users', error); + } } if(isLoading) return
Loading...
; @@ -36,7 +60,7 @@ const UsergroupUsersTable = ({usergroupID}) => { handleAddUser()} + onClick={() => setIsAddUserDialogOpen(true)} icon="plus" buttonStyle={buttonStyle} iconStyle={iconStyle} @@ -44,21 +68,20 @@ const UsergroupUsersTable = ({usergroupID}) => { - + + handleRemoveUser(users[index])} /> - - {/* */}
+ handleAddUser(selectedUsers)} + currentUsers={users} + /> ); } diff --git a/src/pages/usergroups/usergroup.js b/src/pages/usergroups/usergroup.js index 0bc2c30..ab74b17 100644 --- a/src/pages/usergroups/usergroup.js +++ b/src/pages/usergroups/usergroup.js @@ -15,23 +15,55 @@ * along with VILLASweb. If not, see . ******************************************************************************/ +import { useState } from "react"; import { useParams } from "react-router-dom/cjs/react-router-dom.min"; -import { Table, DataColumn, LinkColumn } from "../../common/table"; import { Row, Col } from "react-bootstrap"; import UsergroupScenariosTable from "./tables/usergroup-scenarios-table"; import UsergroupUsersTable from "./tables/usergroup-users-table"; -import { useGetUsergroupByIdQuery } from "../../store/apiSlice"; +import IconButton from "../../common/buttons/icon-button"; +import { buttonStyle, iconStyle } from "./styles"; +import { useGetUsergroupByIdQuery, useUpdateUsergroupMutation } from "../../store/apiSlice"; +import RenameUsergroupDialog from "./dialogs/renameGroupDialog"; -const Usergroup = (props) => { +const Usergroup = () => { const params = useParams(); const usergroupID = params.usergroup; - const {data: {usergroup} = {}, isLoading} = useGetUsergroupByIdQuery(usergroupID); + const {data: {usergroup} = {}, isLoading, refetch} = useGetUsergroupByIdQuery(usergroupID); + const [isRenameModalOpened, setIsRenameModalOpened] = useState(false); + const [updateUsergroup] = useUpdateUsergroupMutation(); + + const handleRename = async (newName) => { + if(newName){ + try { + //update only the name + await updateUsergroup({usergroupID: usergroup.id, usergroup: {name: newName, ScenarioMappings: usergroup.ScenarioMappings}}).unwrap(); + refetch(); + } catch (error) { + console.log('Error updating group', error); + } + } + + setIsRenameModalOpened(false); + } if(isLoading) return
Loading...
; return (
-

{usergroup.name}

+

+ {usergroup.name} + + setIsRenameModalOpened(true)} + icon='edit' + buttonStyle={buttonStyle} + iconStyle={iconStyle} + /> + +

+ @@ -40,6 +72,12 @@ const Usergroup = (props) => { + +
); } diff --git a/src/pages/usergroups/usergroups.js b/src/pages/usergroups/usergroups.js index 942e495..2b56b26 100644 --- a/src/pages/usergroups/usergroups.js +++ b/src/pages/usergroups/usergroups.js @@ -22,6 +22,7 @@ import IconButton from "../../common/buttons/icon-button"; import { buttonStyle, iconStyle } from "./styles"; import AddUsergroupDialog from "./dialogs/addUsergroupDialog"; import DeleteDialog from "../../common/dialogs/delete-dialog"; +import moment from "moment"; const Usergroups = (props) => { const {data: {usergroups} = {}, refetch: refetchUsergroups, isLoading} = useGetUsergroupsQuery(); @@ -60,6 +61,11 @@ const Usergroups = (props) => { setDialogUsergroup({}); setIsDeleteDialogOpen(false); } + + const stateUpdateModifier = (dateString) => { + const date = moment(dateString); + return `${date.fromNow()}`; + }; if(isLoading) return
Loading
; @@ -69,12 +75,12 @@ const Usergroups = (props) => { User Groups setIsAddDialogOpen(true)} - icon="plus" - buttonStyle={buttonStyle} - iconStyle={iconStyle} + childKey={0} + tooltip="Add Usergroup" + onClick={() => setIsAddDialogOpen(true)} + icon="plus" + buttonStyle={buttonStyle} + iconStyle={iconStyle} /> @@ -90,6 +96,11 @@ const Usergroups = (props) => { link="/usergroup/" linkKey="id" /> + stateUpdateModifier(updatedAt)} + /> { handleDeleteUsergroup(isConfirmed)} - /> + title="scenario" + name={dialogUsegroup.name} + show={isDeleteDialogOpen} + onClose={(isConfirmed) => handleDeleteUsergroup(isConfirmed)} + /> ); } } diff --git a/src/store/apiSlice.js b/src/store/apiSlice.js index 57955ee..aa71ef0 100644 --- a/src/store/apiSlice.js +++ b/src/store/apiSlice.js @@ -110,5 +110,8 @@ export const { useAddUsergroupMutation, useDeleteUsergroupMutation, useGetUsergroupByIdQuery, - useGetUsersByUsergroupIdQuery + useGetUsersByUsergroupIdQuery, + useAddUserToUsergroupMutation, + useDeleteUserFromUsergroupMutation, + useUpdateUsergroupMutation } = apiSlice; diff --git a/src/store/endpoints/usergroup-endpoints.js b/src/store/endpoints/usergroup-endpoints.js index 8fbaf9b..65d5d8d 100644 --- a/src/store/endpoints/usergroup-endpoints.js +++ b/src/store/endpoints/usergroup-endpoints.js @@ -31,6 +31,27 @@ export const usergroupEndpoints = (builder) => ({ }, }), }), + updateUsergroup: builder.mutation({ + query: ({ usergroupID, usergroup }) => ({ + url: `/usergroups/${usergroupID}`, + method: 'PUT', + body: { usergroup }, + }), + }), + addUserToUsergroup: builder.mutation({ + query: ({ usergroupID, username }) => ({ + url: `/usergroups/${usergroupID}/user`, + method: 'PUT', + params: { username }, + }), + }), + deleteUserFromUsergroup: builder.mutation({ + query: ({ usergroupID, username }) => ({ + url: `/usergroups/${usergroupID}/user`, + method: 'DELETE', + params: { username }, + }), + }), getUsersByUsergroupId: builder.query({ query: (usergroupID) => `/usergroups/${usergroupID}/users`, }),