diff --git a/db.json b/db.json new file mode 100644 index 0000000..abe49df --- /dev/null +++ b/db.json @@ -0,0 +1,91 @@ +{ + "roles": [ + { + "id": 1, + "name": "oneRole" + }, + { + "id": 2, + "name": "twoRole" + } + ], + "parent": [ + { + "id": 1, + "name": "oneRole" + }, + { + "id": 2, + "name": "twoRole" + } + ], + "institutions": [ + { + "id": 1, + "name": "oneInst" + }, + { + "id": 2, + "name": "twoInst" + } + ], + "users": [ + { + "id": 1, + "Username": "abc", + "name": "abcdef", + "email": "amarthyasa@gmail.com", + "fullname": "abcdef, abc", + "firstName": "abcdef", + "lastName": "abcdef", + "emailPreferences": [], + "institution": "1", + "role_id": "1", + "parent_id": "1", + "email_on_review": true, + "email_on_submission": false, + "email_on_review_of_review": false, + "institution_id": "1" + }, + { + "name": "bcdgf", + "email": "bac@gmail.com", + "fullname": "dac, bac", + "role_id": "1", + "institution_id": "2", + "parent_id": "1", + "email_on_review": true, + "email_on_submission": false, + "email_on_review_of_review": false, + "apiId": "v1", + "id": 5 + } + ], + "participants": [ + { + "name": "asa", + "email": "amarthyasa@gmail.com", + "fullname": "Sivakumar Annu, Amarthya", + "role_id": "2", + "institution_id": "1", + "parent_id": "1", + "email_on_review": true, + "email_on_submission": false, + "email_on_review_of_review": false, + "id": 4 + }, + { + "name": "bcdgf", + "email": "bac@gmail.com", + "fullname": "dac, bac", + "role_id": "1", + "institution_id": "2", + "parent_id": "1", + "email_on_review": true, + "email_on_submission": false, + "email_on_review_of_review": false, + "apiId": "v1", + "id": 5 + } + ] +} \ No newline at end of file diff --git a/routes.json b/routes.json new file mode 100644 index 0000000..4f77947 --- /dev/null +++ b/routes.json @@ -0,0 +1,4 @@ +{ + "users/:id": "api/v1/users/:id", + "users": "api/v1/users" + } \ No newline at end of file diff --git a/src/App.js b/src/App.js index 3132f60..437b69b 100644 --- a/src/App.js +++ b/src/App.js @@ -3,6 +3,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom"; import Home from "./components/Layout/Home"; import RootLayout from "./components/Layout/Root"; import Users from "./components/Users/Users"; +import Participants from "./components/Participants/Participants"; function App() { const router = createBrowserRouter([ @@ -12,6 +13,7 @@ function App() { children: [ { index: true, element: }, { path: "users", element: }, + { path: "participants", element: }, ], }, ]); diff --git a/src/components/Layout/Header.js b/src/components/Layout/Header.js index 3ca8a32..11b4d34 100644 --- a/src/components/Layout/Header.js +++ b/src/components/Layout/Header.js @@ -63,6 +63,9 @@ const Header = () => { Questionnaire + + Participants + Impersonate User diff --git a/src/components/Participants/CreateParticipant.js b/src/components/Participants/CreateParticipant.js new file mode 100644 index 0000000..115f105 --- /dev/null +++ b/src/components/Participants/CreateParticipant.js @@ -0,0 +1,190 @@ +import {Form, Formik} from "formik"; +import {useEffect, useState} from "react"; +import {Button, Col, InputGroup, Modal, Row} from "react-bootstrap"; +import {useDispatch} from "react-redux"; +import * as Yup from "yup"; +import useAPI from "../../hooks/use-api"; +import {alertActions} from "../../store/alert"; +import FormCheckboxGroup from "../UI/Form/FormCheckboxGroup"; +import FormInput from "../UI/Form/FormInput"; +import FormSelect from "../UI/Form/FormSelect"; +import {emailOptions, transformInstitutionsResponse, transformRolesResponse, transformParticipantRequest,} from "./util"; + + +const loggedInParticipant = "1"; + +// initial values for the new participant +const initialValues = { + name: "", + email: "", + firstName: "", + lastName: "", + emailPreferences: [], + institution: "", + role: "", +}; +// Validating if the values entered for the new participant are correct +const validationSchema = Yup.object({ + name: Yup.string() + .required("Required") + .lowercase("Participantname must be lowercase") + .min(3, "Participantname must be at least 3 characters") + .max(20, "Participantname must be at most 20 characters"), + email: Yup.string().required("Required").email("Invalid email format"), + firstName: Yup.string().required("Required").nonNullable(), + lastName: Yup.string().required("Required").nonNullable(), + role: Yup.string().required("Required").nonNullable(), + institution: Yup.string().required("Required").nonNullable(), +}); + + +const CreateParticipant = ({onClose}) => { + const dispatch = useDispatch(); + const [show, setShow] = useState(true); + const {data: roles, sendRequest: fetchRoles} = useAPI(); + const {data: institutions, sendRequest: fetchInstitutions} = useAPI(); + const { + data: createdParticipant, + error: participantError, + sendRequest: createParticipant, + } = useAPI(); + + // Fetching the roles, institutions that need to be listed on the roles, institutions drop down on the create user form + useEffect(() => { + fetchRoles({url: "/roles", transformResponse: transformRolesResponse}); + fetchInstitutions({ + url: "/institutions", + transformResponse: transformInstitutionsResponse, + }); + }, [fetchRoles, fetchInstitutions]); + + useEffect(() => { + if (participantError) { + dispatch(alertActions.showAlert({ + variant: "danger", + message: participantError + })); + } + }, [participantError, dispatch]); + + // if the participant was created, onclose is set to the newly created participant + useEffect(() => { + if (createdParticipant.length > 0) { + setShow(false); + onClose(createdParticipant[0]); + } + }, [participantError, createdParticipant, onClose]); + + /* post request to the API with the new participants values when onSubmit is called + */ + const onSubmit = (values, submitProps) => { + createParticipant({ + url: "/participants", + method: "post", + data: {...values, parent: loggedInParticipant}, + transformRequest: transformParticipantRequest, + }); + submitProps.resetForm(); + submitProps.setSubmitting(false); + }; + + const handleClose = () => { + setShow(false); + onClose(); + }; + + return ( + + + Create Participant + + {/* onSubmit is called when Create Participant button on the create Participant form is clicked */} + + + {(formik) => { + return ( +
+ Role + } + /> + @ + } + /> + + + + + + + + + Institution + + } + /> + + + + + + + ); + }} +
+
+
+ ); +}; + +export default CreateParticipant; diff --git a/src/components/Participants/DeleteParticipant.js b/src/components/Participants/DeleteParticipant.js new file mode 100644 index 0000000..2d71b5f --- /dev/null +++ b/src/components/Participants/DeleteParticipant.js @@ -0,0 +1,66 @@ +import {useEffect, useState} from "react"; +import {Button, Modal} from "react-bootstrap"; +import {useDispatch} from "react-redux"; +import useAPI from "../../hooks/use-api"; +import {alertActions} from "../../store/alert"; + +const DeleteParticipant = ({participantData, onClose}) => { + const dispatch = useDispatch(); + const { + data: deletedParticipant, + error: participantError, + sendRequest: deleteParticipant, + } = useAPI(); + // show initially set to true + const [show, setShow] = useState(true); + + // useAPI called with the `/participants/${participantData.id}` URL + const deleteHandler = () => + deleteParticipant({url: `/participants/${participantData.id}`, method: "DELETE"}); + + useEffect(() => { + if (participantError) { + dispatch(alertActions.showAlert({ + variant: "danger", + message: participantError, + })); + } + }, [participantError, dispatch]); + + // if the participant was deleted, onclose is set to the deleted participant + useEffect(() => { + if (deletedParticipant.length > 0) { + setShow(false); + onClose(deletedParticipant[0]); + } + }, [deletedParticipant, onClose]); + + const closeHandler = () => { + setShow(false); + onClose(); + }; + + // deleteHandler is called with Delete button is clicked and closeHandler is called when Cancel button is clicked + return ( + + + Delete Participant + + +

+ Are you sure you want to delete participant {participantData.name}? +

+
+ + + + +
+ ); +}; + +export default DeleteParticipant; diff --git a/src/components/Participants/Participants.js b/src/components/Participants/Participants.js new file mode 100644 index 0000000..51b34b8 --- /dev/null +++ b/src/components/Participants/Participants.js @@ -0,0 +1,185 @@ +import {useCallback, useEffect, useMemo, useState} from "react"; +import {Button, Col, Container, Row} from "react-bootstrap"; +import {useDispatch} from "react-redux"; +import useAPI from "../../hooks/use-api"; +import {alertActions} from "../../store/alert"; +import {AddUserIcon} from "../UI/Icons"; +import Table from "../UI/Table/Table"; +import CreateParticipant from "./CreateParticipant"; +import DeleteParticipant from "./DeleteParticipant"; +import UpdateParticipant from "./UpdateParticipant"; +import {PARTICIPANT_COLUMNS} from "./participantColumns"; + +const Participants = () => { + const dispatch = useDispatch(); + const { + error, + isLoading, + data: participantData, + setData: setParticipantData, + sendRequest: fetchParticipants, + } = useAPI(); + + const [showCreate, setShowCreate] = useState(false); + const [showUpdate, setShowUpdate] = useState({ + visible: false, + data: {}, + }); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState({ + visible: false, + data: {}, + }); + + useEffect(() => fetchParticipants({url: "/participants", method: "get"}), [fetchParticipants]); + + // Error alert + useEffect(() => { + if (error) { + dispatch(alertActions.showAlert({ + variant: "danger", + message: error + })); + } + }, [error, dispatch]); + + /* After deleting a participant + ParticipantData is reset to include the newly created participant + An alert is dispatched to show that the participant was created successfully + showCreate.visible is set to false to close the create participant form + */ + + const onCreateParticipantHandler = useCallback( + (participant) => { + console.log(participant) + if (participant && participant.name) { + console.log(participant); + setParticipantData((prevData) => [...prevData, participant]); + dispatch(alertActions.showAlert({ + variant: "success", + message: `Participant ${participant.name} created successfully!` + })); + } + setShowCreate(false); + }, + [setParticipantData, dispatch] + ); + + /* After updating a participant + ParticipantData is reset after removing the previous data of the updated participant and adding the new updated participant + An alert is dispatched to show that the participant was updated successfully + showUpdate.visible is set to false to close the update participant form + */ + + const onUpdateParticipantHandler = useCallback( + (updatedParticipant) => { + if (updatedParticipant && updatedParticipant.name !== undefined) { + setParticipantData((prevData) => [ + ...prevData.filter((participant) => participant.id !== updatedParticipant.id), + updatedParticipant, + ]); + dispatch(alertActions.showAlert({ + variant: "success", + message: `Participant ${updatedParticipant.name} updated successfully!` + })); + } + setShowUpdate({visible: false, data: {}}); + }, + [setParticipantData, dispatch] + ); + + /* After deleting a participant + ParticipantData is is returned after removing the deleted participant + An alert is dispatched to show that the participant was deleted successfully + showDeleteConfirmation.visible is set to false to close the delete participant Modal + */ + const onDeleteParticipantHandler = useCallback( + (id, name, status) => { + if (status) { + setParticipantData((prevData) => { + return prevData.filter((participant) => participant.id !== id); + }); + dispatch(alertActions.showAlert({ + variant: "success", + message: `Participant ${name} deleted successfully!` + })); + } + setShowDeleteConfirmation({visible: false, data: {}}); + }, + [setParticipantData, dispatch] + ); + + /* onEditHandle + sets showUpdate.visible to True and sets the row id of the participant that is to be updated when Edit button is clicked + */ + const onEditHandle = (row) => + setShowUpdate({visible: true, data: row.original}); + + /* onDeleteHandle + sets showDeleteConfirmation.visible to True and sets the row id of the participant that is to be deleted when Delete button is clicked + */ + const onDeleteHandle = (row) => + setShowDeleteConfirmation({visible: true, data: row.original}); + + const tableColumns = useMemo( + () => PARTICIPANT_COLUMNS(onDeleteHandle, onEditHandle), + [] + ); + const tableData = useMemo( + () => (isLoading ? [] : participantData), + [participantData, isLoading] + ); + const initialState = {hiddenColumns: ["id", "institution"]}; + + /* Manage Participants page + Displays the "Manage Participants" title and the table with participant information by default. + Displays CreateParticipant form when showCreate is True + Displays UpdateParticipant form when showUpdate.visible is True + Displays DeleteParticipant Modal when showDeleteConfirmation.visible is True + */ + return ( + + + +

Manage Participants

+ +
+
+ + + + + {showCreate && } + {showUpdate.visible && ( + + )} + {showDeleteConfirmation.visible && ( + + )} + + + + + + ); +}; + +export default Participants; diff --git a/src/components/Participants/UpdateParticipant.js b/src/components/Participants/UpdateParticipant.js new file mode 100644 index 0000000..e2fff14 --- /dev/null +++ b/src/components/Participants/UpdateParticipant.js @@ -0,0 +1,200 @@ +import {Form, Formik} from "formik"; +import {useEffect, useState} from "react"; +import {Button, Col, InputGroup, Modal, Row} from "react-bootstrap"; +import {useDispatch} from "react-redux"; +import * as Yup from "yup"; +import useAPI from "../../hooks/use-api"; +import {alertActions} from "../../store/alert"; +import FormCheckboxGroup from "../UI/Form/FormCheckboxGroup"; +import FormInput from "../UI/Form/FormInput"; +import FormSelect from "../UI/Form/FormSelect"; +import {emailOptions, transformInstitutionsResponse, transformRolesResponse, transformParticipantRequest,} from "./util"; + +// Get the logged-in participant from the session +const loggedInParticipant = "1"; +const initialValues = (participant) => { + const [lastName, firstName] = participant.fullname.split(","); + const emailPreferences = [ + "email_on_review", + "email_on_review_of_review", + "email_on_submission", + ].filter((pref) => participant[pref]); + + return { + name: participant.name, + email: participant.email, + firstName: firstName.trim(), + lastName: lastName.trim(), + emailPreferences: emailPreferences, + institution: participant.institution_id.id ? participant.institution_id.id : "", + role: participant.role_id.id, + }; +}; + +const validationSchema = Yup.object({ + name: Yup.string() + .required("Required") + .lowercase("Participantname must be lowercase") + .min(3, "Participantname must be at least 3 characters") + .max(20, "Participantname must be at most 20 characters"), + email: Yup.string().required("Required").email("Invalid email format"), + firstName: Yup.string().required("Required").nonNullable(), + lastName: Yup.string().required("Required").nonNullable(), + role: Yup.string().required("Required").nonNullable(), + institution: Yup.string().required("Required").nonNullable(), +}); + +const UpdateParticipant = ({participantData, onClose}) => { + const [show, setShow] = useState(true); + const {data: roles, sendRequest: fetchRoles} = useAPI(); + const {data: institutions, sendRequest: fetchInstitutions} = useAPI(); + const { + data: updatedParticipant, + error: participantError, + sendRequest: updateParticipant, + } = useAPI(); + const dispatch = useDispatch(); + // Fetching the roles, institutions that need to be listed on the roles, institutions drop down on the create user form + useEffect(() => { + fetchRoles({url: "/roles", transformResponse: transformRolesResponse}); + fetchInstitutions({ + url: "/institutions", + transformResponse: transformInstitutionsResponse, + }); + }, [fetchRoles, fetchInstitutions]); + + // Close the modal if the participant is updated successfully and pass the updated participant to the parent component + useEffect(() => { + if (updatedParticipant.length > 0) { + console.log("participant updated"); + onClose(updatedParticipant[0]); + setShow(false); + } + }, [participantError, updatedParticipant, onClose]); + + useEffect(() => { + if (participantError) { + dispatch(alertActions.showAlert({ + variant: "danger", + message: participantError, + })); + } + }, [participantError, dispatch]); + + /* patch request to the API with the updated participant values when onSubmit is called + */ + const onSubmit = (values, submitProps) => { + const participantId = participantData.id; + updateParticipant({ + url: `/participants/${participantId}`, + method: "patch", + data: {...values, parent: loggedInParticipant}, + transformRequest: transformParticipantRequest, + }); + submitProps.resetForm(); + submitProps.setSubmitting(false); + }; + + const handleClose = () => { + setShow(false); + onClose(); + }; + + return ( + + + Update Participant + + {/* onSubmit is called when Update Participant button on the update Participant form is clicked */} + + {participantError &&

{participantError}

} + + {(formik) => { + return ( +
+ Role + } + /> + @ + } + /> + + + + + + + + Institution + + } + /> + + + + + + + ); + }} +
+
+
+ ); +}; + +export default UpdateParticipant; diff --git a/src/components/Participants/participantColumns.js b/src/components/Participants/participantColumns.js new file mode 100644 index 0000000..92b8cfb --- /dev/null +++ b/src/components/Participants/participantColumns.js @@ -0,0 +1,88 @@ +import {Fragment} from "react"; +import {Button} from "react-bootstrap"; +import {Link} from "react-router-dom"; +import {EditIcon, RemoveUserIcon} from "../UI/Icons"; + +export const PARTICIPANT_COLUMNS = (handleDelete, handleEdit) => [ + { + Header: "Id", + accessor: "id", + disableFilters: true, + }, + { + Header: "Username", + accessor: "name", + Cell: ({row}) => ( + {row.original.name} + ), + }, + { + Header: "Full Name", + accessor: "fullname", + }, + { + Header: "Email", + accessor: "email", + }, + { + Header: "Role", + accessor: (d) => d.role_id.name, + disableFilters: true, + }, + { + Header: "Parent", + accessor: (d) => d.parent_id.name, + disableFilters: true, + }, + { + Header: "Email Preferences", + columns: [ + { + Header: "Review", + accessor: (d) => d.email_on_review.toString(), + disableFilters: true, + }, + { + Header: "Submission", + accessor: (d) => d.email_on_submission.toString(), + disableFilters: true, + }, + { + Header: "Meta Review", + accessor: (d) => d.email_on_review_of_review.toString(), + disableFilters: true, + }, + ], + }, + { + id: "institution", + Header: "Institution", + accessor: (d) => d.institution_id.name, + disableFilters: true, + }, + { + id: "actions", + Header: "Actions", + Cell: ({row}) => { + return ( + + + + + ); + }, + }, +]; diff --git a/src/components/Participants/util.js b/src/components/Participants/util.js new file mode 100644 index 0000000..8b4c391 --- /dev/null +++ b/src/components/Participants/util.js @@ -0,0 +1,75 @@ +export const emailOptions = [ + {label: "When someone else reviews my work", value: "email_on_review"}, + { + label: "When someone else submits work I am assigned to review", + value: "email_on_submission", + }, + { + label: "When someone else reviews one of my reviews (meta-reviews my work)", + value: "email_on_review_of_review", + }, +]; + +export const transformInstitutionsResponse = (institutions) => { + let institutionsData = [{key: "Select an Institution", value: ""}]; + institutions = JSON.parse(institutions); + institutionsData = institutionsData.concat( + institutions.map((institution) => ({ + key: institution.name, + value: institution.id, + })) + ); + return institutionsData; +}; + +export const transformRolesResponse = (roles) => { + let rolesData = [{key: "Select a Role", value: ""}]; + roles = JSON.parse(roles); + rolesData = rolesData.concat( + roles.map((role) => ({ + key: role.name, + value: role.id, + })) + ); + return rolesData; +}; + +export const transformUserRequest = (values, headers) => { + console.log("transformUserRequest", values, headers); + const user = { + name: values.name, + email: values.email, + fullname: values.lastName + ", " + values.firstName, + role_id: values.role, + institution_id: values.institution, + parent_id: values.parent, + email_on_review: values.emailPreferences.includes("email_on_review"), + email_on_submission: values.emailPreferences.includes( + "email_on_submission" + ), + email_on_review_of_review: values.emailPreferences.includes( + "email_on_review_of_review" + ), + }; + return JSON.stringify(user); +}; + +export const transformParticipantRequest = (values, headers) => { + console.log("transformUserRequest", values, headers); + const user = { + name: values.name, + email: values.email, + fullname: values.lastName + ", " + values.firstName, + role_id: values.role, + institution_id: values.institution, + parent_id: values.parent, + email_on_review: values.emailPreferences.includes("email_on_review"), + email_on_submission: values.emailPreferences.includes( + "email_on_submission" + ), + email_on_review_of_review: values.emailPreferences.includes( + "email_on_review_of_review" + ), + }; + return JSON.stringify(user); +}; diff --git a/src/components/UI/Icons.js b/src/components/UI/Icons.js index 57342b5..3b20b89 100644 --- a/src/components/UI/Icons.js +++ b/src/components/UI/Icons.js @@ -16,6 +16,14 @@ export const RemoveUserIcon = () => { /> ); }; +export const RemoveParticipantIcon = () => { + return ( + remove + ); +}; export const AddUserIcon = () => { return ( diff --git a/src/components/Users/CreateUser.js b/src/components/Users/CreateUser.js index 0193b95..981ce5a 100644 --- a/src/components/Users/CreateUser.js +++ b/src/components/Users/CreateUser.js @@ -11,7 +11,7 @@ import FormSelect from "../UI/Form/FormSelect"; import {emailOptions, transformInstitutionsResponse, transformRolesResponse, transformUserRequest,} from "./util"; // Get the logged-in user from the session -const loggedInUser = null; +const loggedInUser = "1"; const initialValues = { name: "", diff --git a/src/components/Users/Users.js b/src/components/Users/Users.js index a9173e4..1063128 100644 --- a/src/components/Users/Users.js +++ b/src/components/Users/Users.js @@ -43,7 +43,9 @@ const Users = () => { }, [error, dispatch]); const onCreateUserHandler = useCallback( + (user) => { + console.log("hey is thos u"); if (user && user.name) { console.log(user); setUserData((prevData) => [...prevData, user]); diff --git a/src/hooks/use-api.js b/src/hooks/use-api.js index 4877f9b..ce72517 100644 --- a/src/hooks/use-api.js +++ b/src/hooks/use-api.js @@ -1,7 +1,7 @@ import axios from "axios"; import {useCallback, useState} from "react"; -axios.defaults.baseURL = "http://localhost:3000/api/v1"; +axios.defaults.baseURL = "http://localhost:3000"; axios.defaults.headers.common["Accept"] = "application/json"; axios.defaults.headers.post["Content-Type"] = "application/json"; axios.defaults.headers.put["Content-Type"] = "application/json"; @@ -19,6 +19,7 @@ const useAPI = () => { axios(requestConfig) .then((response) => { + console.log(response) // if response if from delete request, response.data is null if (response.config && response.config.method === "delete") setData([response.status]);