From d34d7b24f064cc3efea5b86b3c371925d3a72670 Mon Sep 17 00:00:00 2001 From: JamesNg Date: Mon, 21 Jul 2025 23:17:24 -0400 Subject: [PATCH 01/12] first commit --- backend/models/user.model.js | 2 +- .../components/manageProjects/editProject.jsx | 4 + .../manageProjects/editProjectMembers.jsx | 132 ++++++++++++++++++ .../src/components/user-admin/EditUsers.jsx | 2 + client/src/pages/ProjectList.jsx | 3 +- client/src/pages/UserAdmin.jsx | 3 +- 6 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 client/src/components/manageProjects/editProjectMembers.jsx diff --git a/backend/models/user.model.js b/backend/models/user.model.js index 4c2005cc6..57179fb31 100644 --- a/backend/models/user.model.js +++ b/backend/models/user.model.js @@ -37,7 +37,7 @@ const userSchema = mongoose.Schema({ isHflaGithubMember: { type: Boolean }, // pull from API once github handle in place? githubPublic2FA: { type: Boolean }, // does the user have 2FA enabled on their github and membership set to public? availability: { type: String }, // availability to meet outside of hacknight times; string for now, more structured in future - managedProjects: [{ type: String}], // Which projects managed by user. + managedProjects: [{ type: String }], // Which projects managed by user. //currentProject: { type: String } // no longer need this as we can get it from Project Team Member table // password: { type: String, required: true } isActive: { type: Boolean, default: true } diff --git a/client/src/components/manageProjects/editProject.jsx b/client/src/components/manageProjects/editProject.jsx index 784e08cae..e0896ebc3 100644 --- a/client/src/components/manageProjects/editProject.jsx +++ b/client/src/components/manageProjects/editProject.jsx @@ -10,6 +10,7 @@ import EditIcon from '../../svg/Icon_Edit.svg?react'; import PlusIcon from '../../svg/PlusIcon.svg?react'; import { Typography, Box } from '@mui/material'; +import EditProjectMembers from './editProjectMembers'; // Need to hold user state to check which type of user they are and conditionally render editing fields in this component // for user level block access to all except for the ones checked @@ -106,6 +107,9 @@ const EditProject = ({ setFormData={setFormData} /> + {/* Insert Project Members (Event Editors) here */} + + { + console.log(data) + return ( + + {data.map((user, idx) => { + // Destructure user object + const { _id, name, email } = user; + // return projects.length === 0 ? + return ( + + console.log(user)} + > + + + + {`${name.firstName.toUpperCase()} ${name.lastName.toUpperCase()} ( ${email.toUpperCase()} )`} + {/* {`${name.firstName.toUpperCase()} ${name.lastName.toUpperCase()} ( ${email.toUpperCase()} )`} */} + + + + + + ); + })} + + ); +}; + + +const EditProjectMembers = ({ projectToEdit, isEdit }) => { + console.log(projectToEdit) + + + const history = useHistory(); + // ----------------- States ----------------- + const { auth } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [locationType, setLocationType] = useState('remote'); + // State to track the toggling from Project view to Edit Project View via edit icon. + const [editMode, setEditMode] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [email, setEmail] = useState(''); + + const handleOpen = () => setIsModalOpen(true); + const handleClose = () => setIsModalOpen(false) + const checkFields = () => { + history.push('/projects'); + }; + + + const editIcon = () => { + return ( + setEditMode(!editMode)} + > + + + {editMode ? 'Cancel' : 'Edit'} + + + ); + }; + + return ( + + setEditMode(!editMode)} + > + setEmail(e.target.value)} + fullWidth + /> + + + + ) +} + +export default EditProjectMembers \ No newline at end of file diff --git a/client/src/components/user-admin/EditUsers.jsx b/client/src/components/user-admin/EditUsers.jsx index 825ee23dd..d439224fd 100644 --- a/client/src/components/user-admin/EditUsers.jsx +++ b/client/src/components/user-admin/EditUsers.jsx @@ -47,6 +47,8 @@ const EditUsers = ({ setUserManagedProjects(userToEdit.managedProjects); }, [userToEdit]); + console.log(userManagedProjects) + const userProjectsToDisplay = activeProjects.filter((item) => userProjects.includes(item[0]) ); diff --git a/client/src/pages/ProjectList.jsx b/client/src/pages/ProjectList.jsx index 13d2bf6d8..4bbb2eaa1 100644 --- a/client/src/pages/ProjectList.jsx +++ b/client/src/pages/ProjectList.jsx @@ -63,7 +63,8 @@ export default function ProjectList({ auth }) { [projectApiService, user.accessLevel, user.managedProjects] ); - + const projsWithUsers = projects?.filter((project) => project.managedByUsers?.length > 0); + console.log('Projects with users:', projsWithUsers); // Render loading circle until project data is served from API if (!projects) diff --git a/client/src/pages/UserAdmin.jsx b/client/src/pages/UserAdmin.jsx index 460f53aa7..cbf52d562 100644 --- a/client/src/pages/UserAdmin.jsx +++ b/client/src/pages/UserAdmin.jsx @@ -21,7 +21,8 @@ const UserAdmin = () => { const updateUserDb = useCallback( async (user, managedProjects) => { - await userApiService.updateUserDbProjects(user, managedProjects); + console.log(user, managedProjects); + // await userApiService.updateUserDbProjects(user, managedProjects); fetchUsers(); }, [userApiService, fetchUsers] From ec4048cb19dc6daa4072eb6f03d06daddbd83e6c Mon Sep 17 00:00:00 2001 From: JamesNg Date: Mon, 21 Jul 2025 23:34:16 -0400 Subject: [PATCH 02/12] removed previous comment --- client/src/pages/UserAdmin.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/pages/UserAdmin.jsx b/client/src/pages/UserAdmin.jsx index cbf52d562..460f53aa7 100644 --- a/client/src/pages/UserAdmin.jsx +++ b/client/src/pages/UserAdmin.jsx @@ -21,8 +21,7 @@ const UserAdmin = () => { const updateUserDb = useCallback( async (user, managedProjects) => { - console.log(user, managedProjects); - // await userApiService.updateUserDbProjects(user, managedProjects); + await userApiService.updateUserDbProjects(user, managedProjects); fetchUsers(); }, [userApiService, fetchUsers] From 06ce51f7e4c8ede13aaeb4cc7fb6c6537c16d092 Mon Sep 17 00:00:00 2001 From: JamesNg Date: Tue, 9 Sep 2025 17:25:14 -0400 Subject: [PATCH 03/12] first commit --- backend/controllers/user.controller.js | 20 +++++++++++++++++++- backend/routers/projects.router.js | 2 +- backend/routers/users.router.js | 2 ++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js index d6bad1e1d..df511c901 100644 --- a/backend/controllers/user.controller.js +++ b/backend/controllers/user.controller.js @@ -27,6 +27,25 @@ UserController.user_list = async function (req, res) { } }; +UserController.user_by_email = async function (req, res) { + const { headers } = req; + const { email } = req.params; + + console.log('email: ', email); + + if (headers['x-customrequired-header'] !== expectedHeader) { + return res.sendStatus(403); + } + + try { + const user = await User.find({ email }); + return res.status(200).send(user); + } catch (err) { + console.log(err); + return res.sendStatus(400); + } +}; + // Get list of Users with accessLevel 'admin' or 'superadmin' with GET UserController.admin_list = async function (req, res) { const { headers } = req; @@ -232,7 +251,6 @@ UserController.signin = function (req, res) { }; UserController.verifySignIn = async function (req, res) { - let token = req.headers['x-access-token'] || req.headers['authorization']; if (token.startsWith('Bearer ')) { // Remove Bearer from string diff --git a/backend/routers/projects.router.js b/backend/routers/projects.router.js index 16aed3728..70475742a 100644 --- a/backend/routers/projects.router.js +++ b/backend/routers/projects.router.js @@ -12,7 +12,7 @@ router.put('/', ProjectController.pm_filtered_projects); router.post('/', AuthUtil.verifyCookie, ProjectController.create); -router.get('/:ProjectId', ProjectController.project_by_id); +router.get('/:ProjectId', AuthUtil.verifyCookie, ProjectController.project_by_id); router.put('/:ProjectId', AuthUtil.verifyCookie, ProjectController.update); diff --git a/backend/routers/users.router.js b/backend/routers/users.router.js index a20da8f5a..3d48f9a83 100644 --- a/backend/routers/users.router.js +++ b/backend/routers/users.router.js @@ -6,6 +6,8 @@ const { UserController } = require('../controllers'); // The base is /api/users router.get('/', UserController.user_list); +router.get('/:email', UserController.user_by_email); + router.get('/admins', UserController.admin_list); router.get('/projectManagers', UserController.projectManager_list); From 466954932de43ab766005410eb6b255f6b6f7140 Mon Sep 17 00:00:00 2001 From: JamesNg Date: Wed, 10 Sep 2025 14:03:39 -0400 Subject: [PATCH 04/12] second commit --- client/src/api/ProjectApiService.js | 15 + client/src/api/UserApiService.js | 16 + .../manageProjects/editProjectMembers.jsx | 464 +++++++++++++++--- 3 files changed, 420 insertions(+), 75 deletions(-) diff --git a/client/src/api/ProjectApiService.js b/client/src/api/ProjectApiService.js index b2e692462..15879b9c2 100644 --- a/client/src/api/ProjectApiService.js +++ b/client/src/api/ProjectApiService.js @@ -98,6 +98,21 @@ class ProjectApiService { return undefined; } } + + async fetchManagedByUsers(projectId) { + const url = `${this.baseProjectUrl}${projectId}`; + try { + const res = await fetch(url, { + headers: this.headers, + method: 'GET', + }); + return await res.json(); + } catch (error) { + console.error(`fetchManagedByUsers error: ${error}`); + alert('Server not responding. Please refresh the page.'); + return []; + } + } } export default ProjectApiService; diff --git a/client/src/api/UserApiService.js b/client/src/api/UserApiService.js index 3ef938f02..1822fca2b 100644 --- a/client/src/api/UserApiService.js +++ b/client/src/api/UserApiService.js @@ -14,6 +14,22 @@ class UserApiService { const res = await fetch(this.baseUserUrl, { headers: this.headers, }); + console.log(res); + return await res.json(); + } catch (error) { + console.error(`fetchUsers error: ${error}`); + alert('Server not responding. Please refresh the page.'); + } + return []; + } + + // Fetch user by email + async fetchUserByEmail(email) { + try { + const uri = `${this.baseUserUrl}${email}`; + const res = await fetch(uri, { + headers: this.headers, + }); return await res.json(); } catch (error) { console.error(`fetchUsers error: ${error}`); diff --git a/client/src/components/manageProjects/editProjectMembers.jsx b/client/src/components/manageProjects/editProjectMembers.jsx index e39e0a8f0..48da0827f 100644 --- a/client/src/components/manageProjects/editProjectMembers.jsx +++ b/client/src/components/manageProjects/editProjectMembers.jsx @@ -1,94 +1,324 @@ -import { useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import { useForm, useFormState } from 'react-hook-form'; +import { useEffect, useState } from 'react'; import { + Autocomplete, CircularProgress, Typography, Box, - Button, Grid, - Radio, - FormControl, - FormControlLabel, - RadioGroup, TextField, List, ListItem, - ListItemButton,} from "@mui/material"; + ListItemButton, + Modal, +} from "@mui/material"; import useAuth from '../../hooks/useAuth'; import EditIcon from '../../svg/Icon_Edit.svg?react'; -import PlusIcon from '../../svg/PlusIcon.svg?react'; -import ValidatedTextField from '../parts/form/ValidatedTextField'; +import CloseIcon from '@mui/icons-material/Close'; +import DeleteIcon from '@mui/icons-material/Delete'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import CheckCircleOutline from '@mui/icons-material/CheckCircleOutline'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import TitledBox from '../parts/boxes/TitledBox'; -import ChangesModal from '../ChangesModal'; +import { StyledButton } from '../ProjectForm'; +import UserApiService from '../../api/UserApiService'; +const testProject = [ + { + name: "Project1", + managedByUsers: ["1","2"] + } +]; -const ListComponent = ({ data }) => { - console.log(data) - return ( - - {data.map((user, idx) => { - // Destructure user object - const { _id, name, email } = user; - // return projects.length === 0 ? - return ( - - console.log(user)} - > - - - - {`${name.firstName.toUpperCase()} ${name.lastName.toUpperCase()} ( ${email.toUpperCase()} )`} - {/* {`${name.firstName.toUpperCase()} ${name.lastName.toUpperCase()} ( ${email.toUpperCase()} )`} */} - - - - - - ); - })} - - ); +// Test Users Data +const users = [ + { + _id: "1", + name: { + firstName: "Amber", + lastName: "Jones" + }, + email: "amber@hackforla.com" + }, + { + _id: "2", + name: { + firstName: "Bob", + lastName: "Phillips" + }, + email: "Bob@hackforla.com" + }, + { + _id: "3", + name: { + firstName: "Charlie", + lastName: "Murphy" + }, + email: "charlie@hackforla.com" + }, +]; + +const newUser = { + _id: "4", + name: { + firstName: "mock", + lastName: "user" + }, + email: "test4@hackforla.com" }; +const ButtonGroup = ({ btnName1, btnName2, callBackFn1, callBackFn2, isLoading }) => ( + + + + {isLoading ? : `${btnName1}`} + + + + + {btnName2} + + + +); + -const EditProjectMembers = ({ projectToEdit, isEdit }) => { - console.log(projectToEdit) +const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { + const [openModal, setOpenModal] = useState(false); + const [removeConfirmModal, setRemoveConfirmModal] = useState(false); + const [closeConfirmModal, setCloseConfirmModal] = useState(false); + const [showUserInfo, setShowUserInfo] = useState(""); // Store user ID state of selected user to show info + const handleSavePMs = () => { + console.log('Save PMs') + // Insert logic to save to database here + } + + const handleClosePMs = () => setCloseConfirmModal(true); + + const handleCloseOnYes = () => { + setCloseConfirmModal(false); + setEditMode(false); + } - const history = useHistory(); + const handleCloseOnNo = () => setCloseConfirmModal(false); + + const handleRemovePMs = () => { + /** + Add logic to remove PM from project in database here + + */ + + // Show confirmation modal + setRemoveConfirmModal(true); + setOpenModal(false); + + // Auto close confirmation modal after 2 seconds + setTimeout(() => { + setRemoveConfirmModal(false); + }, 2000); + } + + const modalStyle1 = { + position: 'fixed', + top: 0, + left: 0, + width: '100vw', + height: '100vh', + bgcolor: 'transparent', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + zIndex: 1300, + } + + const modalStyle2 = { + alignItems: 'center', + textAlign: 'center', + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 400, + bgcolor: 'background.paper', + border: '2px solid #000', + boxShadow: 'none', + p: 4, + }; + + return ( + + + {data.map((user, idx) => { + // Destructure user object + const { _id, name, email } = user; + return ( + <> + + + { + if (editMode) setShowUserInfo(_id); + }} + > + + + {name.firstName.toUpperCase() + + ' ' + + name.lastName[0].toUpperCase() + '.'} + + + + + {email} + + + {editMode && setOpenModal(true)} />} + {/* Remove Modal */} + + + + + Are you sure you want to remove this user from the project? + + setOpenModal(false)} isLoading={isLoading} /> + + + {/* Remove Confirmation Modal */} + + setRemoveConfirmModal(false)} + sx={modalStyle1} + > + e.stopPropagation()}> + + + User removed from project. + + + + + + + + {/* User information */} + {showUserInfo === _id && + + + + setShowUserInfo("")} /> + + + + + {name.firstName.toUpperCase() + + ' ' + + name.lastName.toUpperCase()} + + + + + {email} + + + + + + } + + ); + })} + + {editMode && } + {/* Close Confirmation Modal */} + + + + + Are you sure you want to close without saving these changes? + + + + + + ); +}; + + +const EditProjectMembers = ({ projectToEdit }) => { // ----------------- States ----------------- const { auth } = useAuth(); const [isLoading, setIsLoading] = useState(false); - const [locationType, setLocationType] = useState('remote'); - // State to track the toggling from Project view to Edit Project View via edit icon. const [editMode, setEditMode] = useState(false); - const [isModalOpen, setIsModalOpen] = useState(false); + const [errorMsg, setErrorMsg] = useState(false); + const [toggleSelect, setToggleSelect] = useState(false); const [email, setEmail] = useState(''); + const [searchedUser, setSearchedUser] = useState({}); + + const [testUsers, setTestUsers] = useState(users); + + // Create new instance of UserApiService class + const userApiService = new UserApiService(); - const handleOpen = () => setIsModalOpen(true); - const handleClose = () => setIsModalOpen(false) - const checkFields = () => { - history.push('/projects'); - }; + useEffect(() => {}, [testUsers]); const editIcon = () => { return ( @@ -106,9 +336,55 @@ const EditProjectMembers = ({ projectToEdit, isEdit }) => { {editMode ? 'Cancel' : 'Edit'} - ); + ) }; + const handleEmailSearch = async (search) => { + // RegEx for valid email check + const emailRegEx = /^((?:[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~]|(?<=^|\.)"|"(?=$|\.|@)|(?<=".*)[ .](?=.*")|(? { + console.log('handleToggleSelect called') + setToggleSelect(!toggleSelect); + + // INSERT logic here to update projectToEdit's managedByUsers array & user's managedProjects array + if (!toggleSelect) { + // Add user to project's managedByUsers array + setTestUsers((prevUsers) => [...prevUsers, newUser]); + } else { + setTestUsers((prevUsers) => prevUsers.filter((user) => user._id !== newUser._id)); + } + } + + return ( { badge={editIcon()} onClick={() => setEditMode(!editMode)} > - setEmail(e.target.value)} - fullWidth + handleEmailSearch(newInputValue)} + renderOption={(props, option) => ( + + + {!toggleSelect ? option : User added to project successfully} + + {/* Icons for adding and confirming email of new user */} + {!toggleSelect ? + : } + + )} + renderInput={(params) => ( + + )} /> - + {/* Display error message */} + {errorMsg && (No account found with this email address)} + + {/* Code for test data */} + + {/* Replace with real data */} - + ) } From 3a3939e9ff1ce8edd689c1027dc3b8e90d0a4fbd Mon Sep 17 00:00:00 2001 From: JamesNg Date: Wed, 10 Sep 2025 17:30:33 -0400 Subject: [PATCH 05/12] update email search component & fix UI --- .../manageProjects/editProjectMembers.jsx | 149 ++++++++++++------ 1 file changed, 98 insertions(+), 51 deletions(-) diff --git a/client/src/components/manageProjects/editProjectMembers.jsx b/client/src/components/manageProjects/editProjectMembers.jsx index 48da0827f..72ab1b76a 100644 --- a/client/src/components/manageProjects/editProjectMembers.jsx +++ b/client/src/components/manageProjects/editProjectMembers.jsx @@ -21,6 +21,7 @@ import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import TitledBox from '../parts/boxes/TitledBox'; import { StyledButton } from '../ProjectForm'; import UserApiService from '../../api/UserApiService'; +import { set } from 'date-fns'; const testProject = [ { @@ -98,6 +99,11 @@ const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { const [closeConfirmModal, setCloseConfirmModal] = useState(false); const [showUserInfo, setShowUserInfo] = useState(""); // Store user ID state of selected user to show info + useEffect(() => { + // Close user info when exiting out of "Edit" mode + setShowUserInfo(""); + }, [editMode]) + const handleSavePMs = () => { console.log('Save PMs') // Insert logic to save to database here @@ -125,7 +131,7 @@ const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { // Auto close confirmation modal after 2 seconds setTimeout(() => { setRemoveConfirmModal(false); - }, 2000); + }, 1500); } const modalStyle1 = { @@ -198,7 +204,6 @@ const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { {email} - {editMode && setOpenModal(true)} />} {/* Remove Modal */} { + {editMode && setOpenModal(true)} />} {/* User information */} @@ -262,14 +268,14 @@ const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { - + {name.firstName.toUpperCase() + ' ' + name.lastName.toUpperCase()} - + {email} @@ -307,7 +313,7 @@ const EditProjectMembers = ({ projectToEdit }) => { const { auth } = useAuth(); const [isLoading, setIsLoading] = useState(false); const [editMode, setEditMode] = useState(false); - const [errorMsg, setErrorMsg] = useState(false); + const [error, setError] = useState(false); const [toggleSelect, setToggleSelect] = useState(false); const [email, setEmail] = useState(''); const [searchedUser, setSearchedUser] = useState({}); @@ -340,12 +346,15 @@ const EditProjectMembers = ({ projectToEdit }) => { }; const handleEmailSearch = async (search) => { + setEmail(search); + // Reset toggleSelect state if user starts typing again + if (toggleSelect) setToggleSelect(false); + // RegEx for valid email check const emailRegEx = /^((?:[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~]|(?<=^|\.)"|"(?=$|\.|@)|(?<=".*)[ .](?=.*")|(? { if (user[0]) { setSearchedUser(user[0]); - setErrorMsg(false); + setError(false); } else { setSearchedUser({}); - setErrorMsg(true); + setError(true); } } catch (err) { - setErrorMsg(true); + setError(true); console.log(err) } } else { - setErrorMsg(false); + setError(false); setSearchedUser({}) } setIsLoading(false); @@ -373,15 +382,20 @@ const EditProjectMembers = ({ projectToEdit }) => { // Handle logic to toggle email selection and adding user to project's managedByUsers const handleToggleSelect = () => { console.log('handleToggleSelect called') - setToggleSelect(!toggleSelect); + setToggleSelect(true); // INSERT logic here to update projectToEdit's managedByUsers array & user's managedProjects array if (!toggleSelect) { // Add user to project's managedByUsers array setTestUsers((prevUsers) => [...prevUsers, newUser]); - } else { - setTestUsers((prevUsers) => prevUsers.filter((user) => user._id !== newUser._id)); } + + // Confirmation message disappears after 1.5 seconds + setTimeout(() => { + setEmail(""); + setSearchedUser({}); + setToggleSelect(false); + }, 1500); } @@ -392,48 +406,38 @@ const EditProjectMembers = ({ projectToEdit }) => { badge={editIcon()} onClick={() => setEditMode(!editMode)} > - handleEmailSearch(newInputValue)} - renderOption={(props, option) => ( - - - {!toggleSelect ? option : User added to project successfully} - - {/* Icons for adding and confirming email of new user */} - {!toggleSelect ? - : } - - )} - renderInput={(params) => ( + {/* Email search componennt */} + + handleEmailSearch(e.target.value)} placeholder="Enter user email address" - InputProps={{ ...params.InputProps, disableUnderline: true }} - sx={{ - '& .MuiInput-underline:before, & .MuiInput-underline:after': { - borderBottom: 'none !important', - }, - '& .MuiInput-root:before, & .MuiInput-root:after': { - borderBottom: 'none !important', - } - }} + value={email} + size="small" /> + + {searchedUser?.email && ( + + + + {!toggleSelect ? searchedUser?.email : User added to project successfully} + + {/* Icons for adding and confirming email of new user */} + {!toggleSelect ? + : } + + )} - /> + {/* Display error message */} - {errorMsg && (No account found with this email address)} + {error && (No account found with this email address)} {/* Code for test data */} @@ -443,4 +447,47 @@ const EditProjectMembers = ({ projectToEdit }) => { ) } -export default EditProjectMembers \ No newline at end of file +export default EditProjectMembers + + + // {/* working component */} + // handleEmailSearch(newInputValue)} + // renderOption={(props, option) => ( + // + // + // {!toggleSelect ? option : User added to project successfully} + // + // {/* Icons for adding and confirming email of new user */} + // {!toggleSelect ? + // : } + // + // )} + // renderInput={(params) => ( + // + // )} + // /> \ No newline at end of file From 6d37192fb25d42f3c333dfb5a544802ad29dc03b Mon Sep 17 00:00:00 2001 From: JamesNg Date: Tue, 16 Sep 2025 17:54:23 -0400 Subject: [PATCH 06/12] edit Cancel functionality to prompt save changes --- .../manageProjects/editProjectMembers.jsx | 149 ++++++++---------- 1 file changed, 63 insertions(+), 86 deletions(-) diff --git a/client/src/components/manageProjects/editProjectMembers.jsx b/client/src/components/manageProjects/editProjectMembers.jsx index 72ab1b76a..90b72a815 100644 --- a/client/src/components/manageProjects/editProjectMembers.jsx +++ b/client/src/components/manageProjects/editProjectMembers.jsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { - Autocomplete, CircularProgress, Typography, Box, @@ -21,7 +20,6 @@ import WarningAmberIcon from '@mui/icons-material/WarningAmber'; import TitledBox from '../parts/boxes/TitledBox'; import { StyledButton } from '../ProjectForm'; import UserApiService from '../../api/UserApiService'; -import { set } from 'date-fns'; const testProject = [ { @@ -31,7 +29,7 @@ const testProject = [ ]; // Test Users Data -const users = [ +const testUsers = [ { _id: "1", name: { @@ -67,14 +65,14 @@ const newUser = { email: "test4@hackforla.com" }; -const ButtonGroup = ({ btnName1, btnName2, callBackFn1, callBackFn2, isLoading }) => ( +const ButtonGroup = ({ btnName1, btnName2, callBackFn1, callBackFn2, isLoading }) => ( callBackFn1(btn)} > {isLoading ? : `${btnName1}`} @@ -93,37 +91,55 @@ const ButtonGroup = ({ btnName1, btnName2, callBackFn1, callBackFn2, isLoading ); -const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { +const ListComponent = ({ projectMembers, editMode, closeConfirmModal, setChangesMade, setCloseConfirmModal, setEditMode, setProjectMembers, isLoading }) => { const [openModal, setOpenModal] = useState(false); const [removeConfirmModal, setRemoveConfirmModal] = useState(false); - const [closeConfirmModal, setCloseConfirmModal] = useState(false); + const [renderedUsers, setRenderedUsers] = useState([]); + const [removeId, setRemoveId] = useState(""); const [showUserInfo, setShowUserInfo] = useState(""); // Store user ID state of selected user to show info useEffect(() => { // Close user info when exiting out of "Edit" mode setShowUserInfo(""); - }, [editMode]) + setRenderedUsers(projectMembers); + + }, [projectMembers, editMode, renderedUsers]) const handleSavePMs = () => { - console.log('Save PMs') - // Insert logic to save to database here + alert('Saved PMs to database') + // Insert logic to save "renderedUsers" to database } const handleClosePMs = () => setCloseConfirmModal(true); - const handleCloseOnYes = () => { + const handleCloseOnYes = () => { + // Discard changes + setChangesMade(false); + // TEMPORARY LOGIC: Resetting renderedUsers to projectMembers + setProjectMembers(testUsers); + /* + Insert actual logic here + + */ + + // Close modal and exit edit mode setCloseConfirmModal(false); setEditMode(false); } const handleCloseOnNo = () => setCloseConfirmModal(false); - const handleRemovePMs = () => { + const handleRemoveConfirm = () => { /** - Add logic to remove PM from project in database here - + Insert logic to remove PM (user) from project in database here */ + setChangesMade(true); + + // Temporary logic to remove user with id from test data + const updatedUsers = renderedUsers.filter(user => user._id !== removeId); + setProjectMembers(updatedUsers); + // Show confirmation modal setRemoveConfirmModal(true); setOpenModal(false); @@ -155,7 +171,7 @@ const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { left: '50%', transform: 'translate(-50%, -50%)', width: 400, - bgcolor: 'background.paper', + bgcolor: 'white', border: '2px solid #000', boxShadow: 'none', p: 4, @@ -164,9 +180,8 @@ const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { return ( - {data.map((user, idx) => { - // Destructure user object - const { _id, name, email } = user; + {renderedUsers.map((user, idx) => { + const { _id, name, email } = user; // destructure user object return ( <> { color: 'primary.main', mx: 0.16, }} - className="search-results-button" > { - if (editMode) setShowUserInfo(_id); + if (editMode && !openModal) setShowUserInfo(_id); }} > - + {name.firstName.toUpperCase() + ' ' + name.lastName[0].toUpperCase() + '.'} @@ -208,23 +222,19 @@ const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { - + Are you sure you want to remove this user from the project? - setOpenModal(false)} isLoading={isLoading} /> + { setShowUserInfo(""); setOpenModal(false); }} isLoading={isLoading} /> {/* Remove Confirmation Modal */} setRemoveConfirmModal(false)} @@ -232,14 +242,14 @@ const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { > e.stopPropagation()}> - + User removed from project. - {editMode && setOpenModal(true)} />} + {editMode && { setOpenModal(true); setRemoveId(_id); }} />} {/* User information */} @@ -252,7 +262,6 @@ const ListComponent = ({ data, editMode, setEditMode, isLoading }) => { borderBottom: 1.6, borderColor: 'grey.300', }} - key={`result_${_id}/${idx}`} > { const [toggleSelect, setToggleSelect] = useState(false); const [email, setEmail] = useState(''); const [searchedUser, setSearchedUser] = useState({}); - - const [testUsers, setTestUsers] = useState(users); + const [closeConfirmModal, setCloseConfirmModal] = useState(false); + const [changesMade, setChangesMade] = useState(false); + const [projectMembers, setProjectMembers] = useState(testUsers); // Replace testUsers with fetched project's managedByUsers // Create new instance of UserApiService class const userApiService = new UserApiService(); + // useEffect(() => { + // // GET project's managedByUsers & set to projectMembers state + // }, []); - useEffect(() => {}, [testUsers]); const editIcon = () => { return ( @@ -335,7 +347,14 @@ const EditProjectMembers = ({ projectToEdit }) => { alignItems: 'center', cursor: 'pointer', }} - onClick={() => setEditMode(!editMode)} + onClick={() => { + if (editMode && changesMade) { + setCloseConfirmModal(true); + } else { + setEditMode(!editMode); + setError(false); + } + }} > @@ -380,14 +399,16 @@ const EditProjectMembers = ({ projectToEdit }) => { } // Handle logic to toggle email selection and adding user to project's managedByUsers - const handleToggleSelect = () => { - console.log('handleToggleSelect called') + const handleToggleSelect = (addedUser) => { setToggleSelect(true); // INSERT logic here to update projectToEdit's managedByUsers array & user's managedProjects array if (!toggleSelect) { // Add user to project's managedByUsers array - setTestUsers((prevUsers) => [...prevUsers, newUser]); + setProjectMembers((prevMembers) => [...prevMembers, addedUser]); + + // Set changes made to true + setChangesMade(true); } // Confirmation message disappears after 1.5 seconds @@ -397,17 +418,16 @@ const EditProjectMembers = ({ projectToEdit }) => { setToggleSelect(false); }, 1500); } - + return ( setEditMode(!editMode)} > {/* Email search componennt */} - + { {!toggleSelect ? searchedUser?.email : User added to project successfully} {/* Icons for adding and confirming email of new user */} - {!toggleSelect ? + {!toggleSelect ? handleToggleSelect(searchedUser)} /> : } @@ -440,54 +460,11 @@ const EditProjectMembers = ({ projectToEdit }) => { {error && (No account found with this email address)} {/* Code for test data */} - + {/* Replace with real data */} ) } -export default EditProjectMembers - - - // {/* working component */} - // handleEmailSearch(newInputValue)} - // renderOption={(props, option) => ( - // - // - // {!toggleSelect ? option : User added to project successfully} - // - // {/* Icons for adding and confirming email of new user */} - // {!toggleSelect ? - // : } - // - // )} - // renderInput={(params) => ( - // - // )} - // /> \ No newline at end of file +export default EditProjectMembers \ No newline at end of file From 00f61db24dd4d9c6b6b228b9c041de5f325a70c2 Mon Sep 17 00:00:00 2001 From: JamesNg Date: Mon, 22 Sep 2025 21:30:39 -0400 Subject: [PATCH 07/12] add logic to enforce role based permissions to component --- .../components/manageProjects/editProjectMembers.jsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/src/components/manageProjects/editProjectMembers.jsx b/client/src/components/manageProjects/editProjectMembers.jsx index 90b72a815..3df1d11c2 100644 --- a/client/src/components/manageProjects/editProjectMembers.jsx +++ b/client/src/components/manageProjects/editProjectMembers.jsx @@ -336,10 +336,13 @@ const EditProjectMembers = ({ projectToEdit }) => { // useEffect(() => { // // GET project's managedByUsers & set to projectMembers state // }, []); - - + + const accessLevel = auth?.user?.accessLevel; + const userId = auth?.user?._id; + + // Edit icon component only avaiable for VRMS admins and project members (users in project) const editIcon = () => { - return ( + return (accessLevel !== 'user' || projectToEdit?.managedByUsers?.includes(userId)) && ( { {editMode ? 'Cancel' : 'Edit'} - ) + ); }; const handleEmailSearch = async (search) => { From 87039584f90a36d4e16a6bb25bd5188a8059267b Mon Sep 17 00:00:00 2001 From: JamesNg Date: Thu, 25 Sep 2025 16:03:17 -0400 Subject: [PATCH 08/12] update fn call on save --- backend/controllers/project.controller.js | 10 ++ backend/routers/projects.router.js | 7 ++ client/src/api/ProjectApiService.js | 11 ++ .../components/manageProjects/editProject.jsx | 2 +- .../manageProjects/editProjectMembers.jsx | 118 +++++++++++++----- 5 files changed, 119 insertions(+), 29 deletions(-) diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index db017c890..a8d0ea636 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -102,4 +102,14 @@ ProjectController.updateManagedByUsers = async function (req, res) { } }; +ProjectController.bulkUpdateManagedByUsers = async function (req, res) { + const { projectId, bulkOps } = req.body; + try { + const result = await Project.bulkWrite(bulkOps); + res.status(200).json(result); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + module.exports = ProjectController; diff --git a/backend/routers/projects.router.js b/backend/routers/projects.router.js index f7d7e45f4..bd7451e94 100644 --- a/backend/routers/projects.router.js +++ b/backend/routers/projects.router.js @@ -19,4 +19,11 @@ router.put('/:ProjectId', AuthUtil.verifyCookie, ProjectController.update); // Update project's managedByUsers in db router.patch('/:ProjectId', AuthUtil.verifyCookie, ProjectController.updateManagedByUsers); +// Bulk update for editing project members +router.patch( + '/:ProjectId/bulk-update', + AuthUtil.verifyCookie, + ProjectController.bulkUpdateManagedByUsers, +); + module.exports = router; diff --git a/client/src/api/ProjectApiService.js b/client/src/api/ProjectApiService.js index b480ff08c..bf71f0938 100644 --- a/client/src/api/ProjectApiService.js +++ b/client/src/api/ProjectApiService.js @@ -134,6 +134,17 @@ class ProjectApiService { return []; } } + + async bulkUpdateManagedByUsers(projectId, bulkOps) { + return fetch( + `${this.baseProjectUrl}${projectId}/bulk-update`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId, bulkOps }), + } + ).then((res) => res.json()); + } } export default ProjectApiService; diff --git a/client/src/components/manageProjects/editProject.jsx b/client/src/components/manageProjects/editProject.jsx index e0896ebc3..f4ad6e309 100644 --- a/client/src/components/manageProjects/editProject.jsx +++ b/client/src/components/manageProjects/editProject.jsx @@ -108,7 +108,7 @@ const EditProject = ({ /> {/* Insert Project Members (Event Editors) here */} - + { +const ListComponent = ({ projectId, projectMembers, setProjectMembers, editMode, closeConfirmModal, setChangesMade, setCloseConfirmModal, setEditMode, isLoading }) => { const [openModal, setOpenModal] = useState(false); const [removeConfirmModal, setRemoveConfirmModal] = useState(false); const [renderedUsers, setRenderedUsers] = useState([]); const [removeId, setRemoveId] = useState(""); - const [showUserInfo, setShowUserInfo] = useState(""); // Store user ID state of selected user to show info + const [selectedUserId, setSelectedUserId] = useState(""); // Store user ID state of selected user to show info + + // Create new instance of ProjectApiService class + const projectApiService = new ProjectApiService(); useEffect(() => { // Close user info when exiting out of "Edit" mode - setShowUserInfo(""); + setSelectedUserId(""); setRenderedUsers(projectMembers); }, [projectMembers, editMode, renderedUsers]) - const handleSavePMs = () => { + const handleSavePMs = async () => { alert('Saved PMs to database') - // Insert logic to save "renderedUsers" to database + // Insert logic to save (update) "renderedUsers" to database + + // Create addedUsers and removedUsers arrays from original projectMembers + const addedUsers = renderedUsers.filter( + newUser => !projectMembers.some(oldUser => oldUser._id === newUser._id) + ); + + const removedUsers = projectMembers.filter( + oldUser => !renderedUsers.some(newUser => newUser._id === oldUser._id) + ); + + try { + // Update using bulkWrite (bulk update) + const addBulkOps = [ + ...addedUsers.map(user => ({ + updateOne: { + filter: { _id: projectId }, + update: { $addToSet: { managedByUsers: user._id } }, + }, + })), + ] + + const removeBulkOps = [ + ...removedUsers.map(user => ({ + updateOne: { + filter: { _id: projectId }, + update: { $pull: { managedByUsers: user._id } }, + }, + })), + ] + + // // Update addedUsers in parallel + // await Promise.all( + // addedUsers.map(userId => + // projectApiService.updateManagedByUsers(projectId, userId, "add") + // ) + // ); + + // // Update removedUsers in parallel + // await Promise.all( + // removedUsers.map(userId => + // projectApiService.updateManagedByUsers(projectId, userId, "remove") + // ) + // ); + + } catch (err) { + console.log(err) + } } const handleClosePMs = () => setCloseConfirmModal(true); @@ -115,12 +160,10 @@ const ListComponent = ({ projectMembers, editMode, closeConfirmModal, setChanges const handleCloseOnYes = () => { // Discard changes setChangesMade(false); - // TEMPORARY LOGIC: Resetting renderedUsers to projectMembers - setProjectMembers(testUsers); - /* - Insert actual logic here - */ + // Resetting renderedUsers to original projectMembers + setProjectMembers(testUsers); // temporary code for test users + // setProjectMembers(projectMembers); // actual code for origina projectMembers // Close modal and exit edit mode setCloseConfirmModal(false); @@ -144,7 +187,7 @@ const ListComponent = ({ projectMembers, editMode, closeConfirmModal, setChanges setRemoveConfirmModal(true); setOpenModal(false); - // Auto close confirmation modal after 2 seconds + // Auto close confirmation modal after 1.5 seconds setTimeout(() => { setRemoveConfirmModal(false); }, 1500); @@ -203,7 +246,7 @@ const ListComponent = ({ projectMembers, editMode, closeConfirmModal, setChanges > { - if (editMode && !openModal) setShowUserInfo(_id); + if (editMode && !openModal) setSelectedUserId(_id); }} > @@ -228,7 +271,7 @@ const ListComponent = ({ projectMembers, editMode, closeConfirmModal, setChanges Are you sure you want to remove this user from the project? - { setShowUserInfo(""); setOpenModal(false); }} isLoading={isLoading} /> + { setSelectedUserId(""); setOpenModal(false); }} isLoading={isLoading} /> {/* Remove Confirmation Modal */} @@ -253,7 +296,7 @@ const ListComponent = ({ projectMembers, editMode, closeConfirmModal, setChanges {/* User information */} - {showUserInfo === _id && + {selectedUserId === _id && - setShowUserInfo("")} /> + setSelectedUserId("")} /> @@ -328,13 +371,34 @@ const EditProjectMembers = ({ projectToEdit }) => { const [searchedUser, setSearchedUser] = useState({}); const [closeConfirmModal, setCloseConfirmModal] = useState(false); const [changesMade, setChangesMade] = useState(false); - const [projectMembers, setProjectMembers] = useState(testUsers); // Replace testUsers with fetched project's managedByUsers - + const [projectMembers, setProjectMembers] = useState(testUsers); + // Replace testUsers with actual project members by using useEffect fetch below - default state: null + // Create new instance of UserApiService class const userApiService = new UserApiService(); // useEffect(() => { - // // GET project's managedByUsers & set to projectMembers state + // // Create an array of projectMembers (users) from project's managedByUsers (user IDs) + // const fetchProjectMembers = async () => { + // if (projectToEdit?.managedByUsers?.length) { + // setIsLoading(true); + // try { + // const members = await Promise.all( + // projectToEdit.managedByUsers.map(async (userId) => { + // const user = await userApiService.fetchUserById(userId); + // return user; + // }) + // ); + // setProjectMembers(members); + // } catch (err) { + // console.log(err) + // } + // setIsLoading(false); + // } else { + // setProjectMembers([]); + // } + // } + // fetchProjectMembers(); // }, []); const accessLevel = auth?.user?.accessLevel; @@ -461,10 +525,8 @@ const EditProjectMembers = ({ projectToEdit }) => { {/* Display error message */} {error && (No account found with this email address)} - {/* Code for test data */} - - {/* Replace with real data */} + ) From 0186e7e3e6bc6eefe9671b52c14054dd64a94f53 Mon Sep 17 00:00:00 2001 From: JamesNg Date: Fri, 26 Sep 2025 17:28:47 -0400 Subject: [PATCH 09/12] set up controller and logic to sync db to UI --- backend/controllers/user.controller.js | 2 - backend/routers/users.router.js | 7 +- client/src/api/UserApiService.js | 16 ++- .../manageProjects/editProjectMembers.jsx | 109 +++++++----------- 4 files changed, 61 insertions(+), 73 deletions(-) diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js index 2a104a20f..5b4dcc290 100644 --- a/backend/controllers/user.controller.js +++ b/backend/controllers/user.controller.js @@ -123,8 +123,6 @@ UserController.user_by_id = async function (req, res) { try { const user = await User.findById(UserId); - // TODO throw 404 if User.findById returns empty object - // and look downstream to see whether 404 would break anything return res.status(200).send(user); } catch (err) { console.error(err); diff --git a/backend/routers/users.router.js b/backend/routers/users.router.js index d92aa1ca4..dbd634f0f 100644 --- a/backend/routers/users.router.js +++ b/backend/routers/users.router.js @@ -6,7 +6,9 @@ const { UserController } = require('../controllers'); // The base is /api/users router.get('/', UserController.user_list); -router.get('/:email', UserController.user_by_email); +router.get('/id/:UserId', UserController.user_by_id); + +router.get('/email/:email', UserController.user_by_email); router.get('/admins', UserController.admin_list); @@ -14,11 +16,8 @@ router.get('/projectManagers', UserController.projectManager_list); router.post('/', UserController.create); -router.get('/:UserId', UserController.user_by_id); - router.patch('/:UserId', UserController.update); -// Update user projects in db router.patch('/:UserId/managedProjects', UserController.updateManagedProjects); router.delete('/:UserId', UserController.delete); diff --git a/client/src/api/UserApiService.js b/client/src/api/UserApiService.js index f61df9bb3..a054385ee 100644 --- a/client/src/api/UserApiService.js +++ b/client/src/api/UserApiService.js @@ -23,10 +23,24 @@ class UserApiService { return []; } + async fetchUserById(id) { + try { + const uri = `${this.baseUserUrl}id/${id}`; + const res = await fetch(uri, { + headers: this.headers, + }); + return await res.json(); + } catch (error) { + console.error(`fetchUsers error: ${error}`); + alert('Server not responding. Please refresh the page.'); + } + return []; + } + // Fetch user by email async fetchUserByEmail(email) { try { - const uri = `${this.baseUserUrl}${email}`; + const uri = `${this.baseUserUrl}email/${email}`; const res = await fetch(uri, { headers: this.headers, }); diff --git a/client/src/components/manageProjects/editProjectMembers.jsx b/client/src/components/manageProjects/editProjectMembers.jsx index ab96274a2..90b557668 100644 --- a/client/src/components/manageProjects/editProjectMembers.jsx +++ b/client/src/components/manageProjects/editProjectMembers.jsx @@ -86,22 +86,20 @@ const ButtonGroup = ({ btnName1, btnName2, callBackFn1, callBackFn2, isLoading } ); -const ListComponent = ({ projectId, projectMembers, setProjectMembers, editMode, closeConfirmModal, setChangesMade, setCloseConfirmModal, setEditMode, isLoading }) => { +const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUsers, editMode, closeConfirmModal, setChangesMade, setCloseConfirmModal, setEditMode, isLoading }) => { const [openModal, setOpenModal] = useState(false); const [removeConfirmModal, setRemoveConfirmModal] = useState(false); - const [renderedUsers, setRenderedUsers] = useState([]); const [removeId, setRemoveId] = useState(""); const [selectedUserId, setSelectedUserId] = useState(""); // Store user ID state of selected user to show info - // Create new instance of ProjectApiService class + // Create new instance of ProjectApiService class to access backend routers & controllers const projectApiService = new ProjectApiService(); - useEffect(() => { - // Close user info when exiting out of "Edit" mode - setSelectedUserId(""); - setRenderedUsers(projectMembers); + console.log('Initial projectMembers:', projectMembers) - }, [projectMembers, editMode, renderedUsers]) + useEffect(() => { + setSelectedUserId(""); // close user info when exiting out of "Edit" mode + }, [projectMembers, editMode]) const handleSavePMs = async () => { alert('Saved PMs to database') @@ -111,7 +109,6 @@ const ListComponent = ({ projectId, projectMembers, setProjectMembers, editMode, const addedUsers = renderedUsers.filter( newUser => !projectMembers.some(oldUser => oldUser._id === newUser._id) ); - const removedUsers = projectMembers.filter( oldUser => !renderedUsers.some(newUser => newUser._id === oldUser._id) ); @@ -158,35 +155,22 @@ const ListComponent = ({ projectId, projectMembers, setProjectMembers, editMode, const handleClosePMs = () => setCloseConfirmModal(true); const handleCloseOnYes = () => { - // Discard changes - setChangesMade(false); - - // Resetting renderedUsers to original projectMembers - setProjectMembers(testUsers); // temporary code for test users - // setProjectMembers(projectMembers); // actual code for origina projectMembers - - // Close modal and exit edit mode - setCloseConfirmModal(false); + setChangesMade(false); // Discard changes + setRenderedUsers(projectMembers); // Reset renderedUsers to original projectMembers + setCloseConfirmModal(false); // Close modal and exit edit mode setEditMode(false); } const handleCloseOnNo = () => setCloseConfirmModal(false); const handleRemoveConfirm = () => { - /** - Insert logic to remove PM (user) from project in database here - */ - setChangesMade(true); - - // Temporary logic to remove user with id from test data + // Remove user from renderedUsers state to update UI const updatedUsers = renderedUsers.filter(user => user._id !== removeId); - setProjectMembers(updatedUsers); - + setRenderedUsers(updatedUsers); // Show confirmation modal setRemoveConfirmModal(true); setOpenModal(false); - // Auto close confirmation modal after 1.5 seconds setTimeout(() => { setRemoveConfirmModal(false); @@ -371,35 +355,34 @@ const EditProjectMembers = ({ projectToEdit }) => { const [searchedUser, setSearchedUser] = useState({}); const [closeConfirmModal, setCloseConfirmModal] = useState(false); const [changesMade, setChangesMade] = useState(false); - const [projectMembers, setProjectMembers] = useState(testUsers); - // Replace testUsers with actual project members by using useEffect fetch below - default state: null - - // Create new instance of UserApiService class + const [projectMembers, setProjectMembers] = useState([]); + const [renderedUsers, setRenderedUsers] = useState([]); + + // Create new instance of UserApiService class to access backend routers & controllers const userApiService = new UserApiService(); - // useEffect(() => { - // // Create an array of projectMembers (users) from project's managedByUsers (user IDs) - // const fetchProjectMembers = async () => { - // if (projectToEdit?.managedByUsers?.length) { - // setIsLoading(true); - // try { - // const members = await Promise.all( - // projectToEdit.managedByUsers.map(async (userId) => { - // const user = await userApiService.fetchUserById(userId); - // return user; - // }) - // ); - // setProjectMembers(members); - // } catch (err) { - // console.log(err) - // } - // setIsLoading(false); - // } else { - // setProjectMembers([]); - // } - // } - // fetchProjectMembers(); - // }, []); + useEffect(() => { + // Create an array of projectMembers (users) from project's managedByUsers (user IDs) + const fetchProjectMembers = async () => { + if (projectToEdit?.managedByUsers?.length) { + setIsLoading(true); + try { + const members = await Promise.all( + projectToEdit.managedByUsers.map(async (userId) => { + const user = await userApiService.fetchUserById(userId.toString()); + return user; + }) + ); + setProjectMembers(members); + setRenderedUsers(members); + } catch (err) { + console.log(err) + } + setIsLoading(false); + } + } + if (!changesMade) fetchProjectMembers(); + }, [changesMade]); const accessLevel = auth?.user?.accessLevel; const userId = auth?.user?._id; @@ -466,18 +449,12 @@ const EditProjectMembers = ({ projectToEdit }) => { } // Handle logic to toggle email selection and adding user to project's managedByUsers - const handleToggleSelect = (addedUser) => { + const handleAddUser = (addedUser) => { setToggleSelect(true); - - // INSERT logic here to update projectToEdit's managedByUsers array & user's managedProjects array if (!toggleSelect) { - // Add user to project's managedByUsers array - setProjectMembers((prevMembers) => [...prevMembers, addedUser]); - - // Set changes made to true - setChangesMade(true); + setRenderedUsers((prevMembers) => [...prevMembers, addedUser]); // Add user to project's managedByUsers array + setChangesMade(true); // Set changes made to true } - // Confirmation message disappears after 1.5 seconds setTimeout(() => { setEmail(""); @@ -517,7 +494,7 @@ const EditProjectMembers = ({ projectToEdit }) => { {!toggleSelect ? searchedUser?.email : User added to project successfully} {/* Icons for adding and confirming email of new user */} - {!toggleSelect ? handleToggleSelect(searchedUser)} /> + {!toggleSelect ? handleAddUser(searchedUser)} /> : } @@ -525,8 +502,8 @@ const EditProjectMembers = ({ projectToEdit }) => { {/* Display error message */} {error && (No account found with this email address)} - {/* Code for test data */} - + {/* Display users */} + ) From b78461cb26229cd5789efd53cd2cf3f1570c8aaf Mon Sep 17 00:00:00 2001 From: JamesNg Date: Sun, 28 Sep 2025 13:34:36 -0400 Subject: [PATCH 10/12] implement save functionality to persist in db --- backend/controllers/project.controller.js | 6 +- backend/controllers/user.controller.js | 10 + backend/routers/projects.router.js | 6 +- backend/routers/users.router.js | 2 + client/src/api/ProjectApiService.js | 21 +- client/src/api/UserApiService.js | 15 ++ .../manageProjects/editProjectMembers.jsx | 199 +++++++++--------- 7 files changed, 140 insertions(+), 119 deletions(-) diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index a8d0ea636..9f7193227 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -87,11 +87,11 @@ ProjectController.updateManagedByUsers = async function (req, res) { managedProjects = managedProjects.filter((id) => id !== ProjectId); } - // Update project's managedByUsers + // Update project's managedByUsers project.managedByUsers = managedByUsers; await project.save({ validateBeforeSave: false }); - // Update user's managedProjects + // Update user's managedProjects user.managedProjects = managedProjects; await user.save({ validateBeforeSave: false }); @@ -103,7 +103,7 @@ ProjectController.updateManagedByUsers = async function (req, res) { }; ProjectController.bulkUpdateManagedByUsers = async function (req, res) { - const { projectId, bulkOps } = req.body; + const { bulkOps } = req.body; try { const result = await Project.bulkWrite(bulkOps); res.status(200).json(result); diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js index 5b4dcc290..0b022327c 100644 --- a/backend/controllers/user.controller.js +++ b/backend/controllers/user.controller.js @@ -326,4 +326,14 @@ UserController.updateManagedProjects = async function (req, res) { } }; +UserController.bulkUpdateManagedProjects = async function (req, res) { + const { bulkOps } = req.body; + try { + const result = await User.bulkWrite(bulkOps); + res.status(200).json(result); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + module.exports = UserController; diff --git a/backend/routers/projects.router.js b/backend/routers/projects.router.js index bd7451e94..c60dd113d 100644 --- a/backend/routers/projects.router.js +++ b/backend/routers/projects.router.js @@ -20,10 +20,6 @@ router.put('/:ProjectId', AuthUtil.verifyCookie, ProjectController.update); router.patch('/:ProjectId', AuthUtil.verifyCookie, ProjectController.updateManagedByUsers); // Bulk update for editing project members -router.patch( - '/:ProjectId/bulk-update', - AuthUtil.verifyCookie, - ProjectController.bulkUpdateManagedByUsers, -); +router.post('/bulk-updates', AuthUtil.verifyCookie, ProjectController.bulkUpdateManagedByUsers); module.exports = router; diff --git a/backend/routers/users.router.js b/backend/routers/users.router.js index dbd634f0f..bf6306442 100644 --- a/backend/routers/users.router.js +++ b/backend/routers/users.router.js @@ -16,6 +16,8 @@ router.get('/projectManagers', UserController.projectManager_list); router.post('/', UserController.create); +router.post('/bulk-updates', UserController.bulkUpdateManagedProjects); + router.patch('/:UserId', UserController.update); router.patch('/:UserId/managedProjects', UserController.updateManagedProjects); diff --git a/client/src/api/ProjectApiService.js b/client/src/api/ProjectApiService.js index bf71f0938..9160fe7e4 100644 --- a/client/src/api/ProjectApiService.js +++ b/client/src/api/ProjectApiService.js @@ -131,19 +131,22 @@ class ProjectApiService { } catch (error) { console.error(`fetchManagedByUsers error: ${error}`); alert('Server not responding. Please refresh the page.'); - return []; } } - async bulkUpdateManagedByUsers(projectId, bulkOps) { - return fetch( - `${this.baseProjectUrl}${projectId}/bulk-update`, - { + async bulkUpdateManagedByUsers(bulkOps) { + const url = `${this.baseProjectUrl}bulk-updates`; + try { + const res = await fetch(url, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ projectId, bulkOps }), - } - ).then((res) => res.json()); + headers: this.headers, + body: JSON.stringify({ bulkOps }), + }); + return await res.json(); + } catch (error) { + console.error(`bulkUpdateManagedByUsers error: ${error}`); + alert('Server not responding. Please refresh the page.'); + } } } diff --git a/client/src/api/UserApiService.js b/client/src/api/UserApiService.js index a054385ee..1682cfc9d 100644 --- a/client/src/api/UserApiService.js +++ b/client/src/api/UserApiService.js @@ -148,6 +148,21 @@ class UserApiService { alert('server not responding. Please try again.'); } } + + async bulkUpdateManagedProjects(bulkOps) { + const url = `${this.baseUserUrl}bulk-updates`; + try { + const res = await fetch(url, { + method: 'POST', + headers: this.headers, + body: JSON.stringify({ bulkOps }), + }); + return await res.json(); + } catch (error) { + console.error(`bulkUpdateManagedProjects error: ${error}`); + alert('Server not responding. Please try again.'); + } + } } export default UserApiService; diff --git a/client/src/components/manageProjects/editProjectMembers.jsx b/client/src/components/manageProjects/editProjectMembers.jsx index 90b557668..802b00c85 100644 --- a/client/src/components/manageProjects/editProjectMembers.jsx +++ b/client/src/components/manageProjects/editProjectMembers.jsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from 'react'; -import { +import { useEffect, useState } from 'react'; +import { CircularProgress, Typography, Box, @@ -22,46 +22,11 @@ import UserApiService from '../../api/UserApiService'; import ProjectApiService from '../../api/ProjectApiService'; import { StyledButton } from '../ProjectForm'; - -// Test Users Data -const testUsers = [ - { - _id: "1", - name: { - firstName: "Amber", - lastName: "Jones" - }, - email: "amber@hackforla.com" - }, - { - _id: "2", - name: { - firstName: "Bob", - lastName: "Phillips" - }, - email: "Bob@hackforla.com" - }, - { - _id: "3", - name: { - firstName: "Charlie", - lastName: "Murphy" - }, - email: "charlie@hackforla.com" - }, -]; - -const newUser = { - _id: "4", - name: { - firstName: "mock", - lastName: "user" - }, - email: "test4@hackforla.com" -}; +const projectApiService = new ProjectApiService(); +const userApiService = new UserApiService(); const ButtonGroup = ({ btnName1, btnName2, callBackFn1, callBackFn2, isLoading }) => ( - + { +const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUsers, editMode, closeConfirmModal, setChangesMade, setCloseConfirmModal, setEditMode, isLoading, setIsLoading }) => { const [openModal, setOpenModal] = useState(false); const [removeConfirmModal, setRemoveConfirmModal] = useState(false); const [removeId, setRemoveId] = useState(""); const [selectedUserId, setSelectedUserId] = useState(""); // Store user ID state of selected user to show info - - // Create new instance of ProjectApiService class to access backend routers & controllers - const projectApiService = new ProjectApiService(); - console.log('Initial projectMembers:', projectMembers) + console.log('Initial projectMembers:', renderedUsers) useEffect(() => { setSelectedUserId(""); // close user info when exiting out of "Edit" mode }, [projectMembers, editMode]) const handleSavePMs = async () => { - alert('Saved PMs to database') - // Insert logic to save (update) "renderedUsers" to database - // Create addedUsers and removedUsers arrays from original projectMembers const addedUsers = renderedUsers.filter( newUser => !projectMembers.some(oldUser => oldUser._id === newUser._id) @@ -113,26 +72,65 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs oldUser => !renderedUsers.some(newUser => newUser._id === oldUser._id) ); + if (addedUsers.length === 0 && removedUsers.length === 0) { + // No changes made, exit edit mode + console.log('No changes made'); + setEditMode(false); + setChangesMade(false); + return; + } + + // Use bulkWrite as opposed to a loop with Promise.all & controller for runtime and network efficiency try { - // Update using bulkWrite (bulk update) - const addBulkOps = [ + setIsLoading(true); + + const addUsersToProjBulkOps = [ ...addedUsers.map(user => ({ + // Update manageByUsers array for project updateOne: { filter: { _id: projectId }, update: { $addToSet: { managedByUsers: user._id } }, }, })), ] - - const removeBulkOps = [ + const removeUsersFromProjBulkOps = [ ...removedUsers.map(user => ({ updateOne: { filter: { _id: projectId }, update: { $pull: { managedByUsers: user._id } }, }, - })), + })), + ] + + const projBulkOps = [...addUsersToProjBulkOps, ...removeUsersFromProjBulkOps]; + + const addProjToUserBulkOps = [ + ...addedUsers.map(user => ({ + // Update managedProjects array for user + updateOne: { + filter: { _id: user._id }, + update: { $addToSet: { managedProjects: projectId } }, + }, + })), + ] + const removeProjFromUserBulkOps = [ + ...removedUsers.map(user => ({ + updateOne: { + filter: { _id: user._id }, + update: { $pull: { managedProjects: projectId } }, + }, + })), ] + const userBulkOps = [...addProjToUserBulkOps, ...removeProjFromUserBulkOps]; + + await projectApiService.bulkUpdateManagedByUsers(projBulkOps); // Bulk update of each project's managedByUsers + await userApiService.bulkUpdateManagedProjects(userBulkOps); // Bulk update of each user's managedProjects + + setChangesMade(false); // Reset changes made + setEditMode(false); // Exit edit mode + setIsLoading(false); + // // Update addedUsers in parallel // await Promise.all( // addedUsers.map(userId => @@ -146,15 +144,14 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs // projectApiService.updateManagedByUsers(projectId, userId, "remove") // ) // ); - } catch (err) { console.log(err) } } - + const handleClosePMs = () => setCloseConfirmModal(true); - - const handleCloseOnYes = () => { + + const handleCloseOnYes = () => { setChangesMade(false); // Discard changes setRenderedUsers(projectMembers); // Reset renderedUsers to original projectMembers setCloseConfirmModal(false); // Close modal and exit edit mode @@ -228,7 +225,7 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs mx: 0.16, }} > - { if (editMode && !openModal) setSelectedUserId(_id); }} @@ -259,11 +256,11 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs {/* Remove Confirmation Modal */} - - setRemoveConfirmModal(false)} sx={modalStyle1} > @@ -281,7 +278,7 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs {/* User information */} {selectedUserId === _id && - - - - setSelectedUserId("")} /> - - - - - {name.firstName.toUpperCase() + - ' ' + - name.lastName.toUpperCase()} - - - - - {email} - - + + + setSelectedUserId("")} /> + + + + + {name.firstName.toUpperCase() + + ' ' + + name.lastName.toUpperCase()} + + + + + {email} + - + + } @@ -325,7 +322,7 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs {editMode && } {/* Close Confirmation Modal */} - { const [projectMembers, setProjectMembers] = useState([]); const [renderedUsers, setRenderedUsers] = useState([]); - // Create new instance of UserApiService class to access backend routers & controllers - const userApiService = new UserApiService(); + console.log('changesMade: ', changesMade) useEffect(() => { // Create an array of projectMembers (users) from project's managedByUsers (user IDs) const fetchProjectMembers = async () => { - if (projectToEdit?.managedByUsers?.length) { + if (projectToEdit?.managedByUsers?.length) { setIsLoading(true); try { const members = await Promise.all( @@ -375,7 +371,7 @@ const EditProjectMembers = ({ projectToEdit }) => { ); setProjectMembers(members); setRenderedUsers(members); - } catch (err) { + } catch (err) { console.log(err) } setIsLoading(false); @@ -383,10 +379,10 @@ const EditProjectMembers = ({ projectToEdit }) => { } if (!changesMade) fetchProjectMembers(); }, [changesMade]); - + const accessLevel = auth?.user?.accessLevel; const userId = auth?.user?._id; - + // Edit icon component only avaiable for VRMS admins and project members (users in project) const editIcon = () => { return (accessLevel !== 'user' || projectToEdit?.managedByUsers?.includes(userId)) && ( @@ -414,18 +410,17 @@ const EditProjectMembers = ({ projectToEdit }) => { ); }; - const handleEmailSearch = async (search) => { + const handleEmailSearch = async (search) => { setEmail(search); // Reset toggleSelect state if user starts typing again if (toggleSelect) setToggleSelect(false); // RegEx for valid email check const emailRegEx = /^((?:[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~]|(?<=^|\.)"|"(?=$|\.|@)|(?<=".*)[ .](?=.*")|(? { {/* Email search componennt */} - handleEmailSearch(e.target.value)} - placeholder="Enter user email address" - value={email} + placeholder="Enter user email address" + value={email} size="small" /> @@ -495,7 +490,7 @@ const EditProjectMembers = ({ projectToEdit }) => { {/* Icons for adding and confirming email of new user */} {!toggleSelect ? handleAddUser(searchedUser)} /> - : } + : } )} @@ -503,7 +498,7 @@ const EditProjectMembers = ({ projectToEdit }) => { {/* Display error message */} {error && (No account found with this email address)} {/* Display users */} - + ) From cf194d222d5bf43ba87ed95b524e6c0eab2f86f2 Mon Sep 17 00:00:00 2001 From: JamesNg Date: Mon, 29 Sep 2025 17:19:18 -0400 Subject: [PATCH 11/12] configure proper rendering of proj members after save, refactored components in folder --- .../manageProjects/editPMs/buttonGroup.jsx | 29 +++ .../editPMs/editProjectMembers.jsx | 183 ++++++++++++++ .../projectMembersList.jsx} | 232 +----------------- .../components/manageProjects/editProject.jsx | 2 +- 4 files changed, 224 insertions(+), 222 deletions(-) create mode 100644 client/src/components/manageProjects/editPMs/buttonGroup.jsx create mode 100644 client/src/components/manageProjects/editPMs/editProjectMembers.jsx rename client/src/components/manageProjects/{editProjectMembers.jsx => editPMs/projectMembersList.jsx} (57%) diff --git a/client/src/components/manageProjects/editPMs/buttonGroup.jsx b/client/src/components/manageProjects/editPMs/buttonGroup.jsx new file mode 100644 index 000000000..29727dbd3 --- /dev/null +++ b/client/src/components/manageProjects/editPMs/buttonGroup.jsx @@ -0,0 +1,29 @@ +import { CircularProgress, Grid } from "@mui/material"; +import { StyledButton } from '../../ProjectForm'; + +const ButtonGroup = ({ btnName1, btnName2, callBackFn1, callBackFn2, isLoading }) => ( + + + callBackFn1(btn)} + > + {isLoading ? : `${btnName1}`} + + + + + {btnName2} + + + +); + +export default ButtonGroup \ No newline at end of file diff --git a/client/src/components/manageProjects/editPMs/editProjectMembers.jsx b/client/src/components/manageProjects/editPMs/editProjectMembers.jsx new file mode 100644 index 000000000..1ec35e78b --- /dev/null +++ b/client/src/components/manageProjects/editPMs/editProjectMembers.jsx @@ -0,0 +1,183 @@ +import { useEffect, useState } from 'react'; +import { + Typography, + Box, + Grid, + TextField, +} from "@mui/material"; +import useAuth from '../../../hooks/useAuth'; +import EditIcon from '../../../svg/Icon_Edit.svg?react'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import CheckCircleOutline from '@mui/icons-material/CheckCircleOutline'; +import TitledBox from '../../parts/boxes/TitledBox'; +import UserApiService from '../../../api/UserApiService'; +import ProjectMembersList from './projectMembersList' + +const userApiService = new UserApiService(); + +const EditProjectMembers = ({ projectToEdit }) => { + // ----------------- States ----------------- + const { auth } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [editMode, setEditMode] = useState(false); + const [error, setError] = useState(""); + const [toggleSelect, setToggleSelect] = useState(false); + const [email, setEmail] = useState(''); + const [searchedUser, setSearchedUser] = useState({}); + const [closeConfirmModal, setCloseConfirmModal] = useState(false); + const [changesMade, setChangesMade] = useState(false); + const [projectMembers, setProjectMembers] = useState([]); + const [renderedUsers, setRenderedUsers] = useState([]); + + useEffect(() => { + // Create an array of projectMembers (users) from project's managedByUsers (user IDs) + const fetchProjectMembers = async () => { + if (projectToEdit?.managedByUsers?.length) { + setIsLoading(true); + try { + const members = await Promise.all( + projectToEdit.managedByUsers.map(async (userId) => { + const user = await userApiService.fetchUserById(userId.toString()); + return user; + }) + ); + setProjectMembers(members); + setRenderedUsers(members); + } catch (err) { + console.log(err) + } + setIsLoading(false); + } + } + if (!changesMade) fetchProjectMembers(); + }, []); + + const accessLevel = auth?.user?.accessLevel; + const userId = auth?.user?._id; + + // Edit icon component only avaiable for VRMS admins and project members (users in project) + const editIcon = () => { + return (accessLevel !== 'user' || projectToEdit?.managedByUsers?.includes(userId)) && ( + { + if (editMode && changesMade) { + setCloseConfirmModal(true); + } else { + setEditMode(!editMode); + setError(""); + } + }} + > + + + {editMode ? 'Cancel' : 'Edit'} + + + ); + }; + + const handleEmailSearch = async (search) => { + setEmail(search); + // Reset toggleSelect state if user starts typing again + if (toggleSelect) setToggleSelect(false); + + // RegEx for valid email check + const emailRegEx = /^((?:[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~]|(?<=^|\.)"|"(?=$|\.|@)|(?<=".*)[ .](?=.*")|(? { + // Check if user is already in renderedUsers + if (renderedUsers.some(user => user._id === addedUser._id)) { + setError("The user has already been added"); + return; + } + + setToggleSelect(true); + if (!toggleSelect) { + setRenderedUsers((prevUsers) => [...prevUsers, addedUser]); // Add user to project's managedByUsers array + setChangesMade(true); // Set changes made to true + } + // Confirmation message disappears after 1.5 seconds + setTimeout(() => { + setEmail(""); + setSearchedUser({}); + setToggleSelect(false); + }, 1500); + } + + + return ( + + + {/* Email search componennt */} + + + handleEmailSearch(e.target.value)} + placeholder="Enter user email address" + value={email} + size="small" + /> + + {searchedUser?.email && ( + + + + {!toggleSelect ? searchedUser?.email : User added to project successfully} + + {/* Icons for adding and confirming email of new user */} + {!toggleSelect ? handleAddUser(searchedUser)} /> + : } + + + )} + + {/* Display error message */} + {error && ({error})} + {/* Display project members */} + + + + ) +} + +export default EditProjectMembers \ No newline at end of file diff --git a/client/src/components/manageProjects/editProjectMembers.jsx b/client/src/components/manageProjects/editPMs/projectMembersList.jsx similarity index 57% rename from client/src/components/manageProjects/editProjectMembers.jsx rename to client/src/components/manageProjects/editPMs/projectMembersList.jsx index 802b00c85..120f8885d 100644 --- a/client/src/components/manageProjects/editProjectMembers.jsx +++ b/client/src/components/manageProjects/editPMs/projectMembersList.jsx @@ -1,64 +1,31 @@ import { useEffect, useState } from 'react'; import { - CircularProgress, Typography, Box, Grid, - TextField, List, ListItem, ListItemButton, Modal, } from "@mui/material"; -import useAuth from '../../hooks/useAuth'; -import EditIcon from '../../svg/Icon_Edit.svg?react'; -import CloseIcon from '@mui/icons-material/Close'; +import ButtonGroup from './buttonGroup' import DeleteIcon from '@mui/icons-material/Delete'; -import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; -import CheckCircleOutline from '@mui/icons-material/CheckCircleOutline'; +import CloseIcon from '@mui/icons-material/Close'; import WarningAmberIcon from '@mui/icons-material/WarningAmber'; -import TitledBox from '../parts/boxes/TitledBox'; -import UserApiService from '../../api/UserApiService'; -import ProjectApiService from '../../api/ProjectApiService'; -import { StyledButton } from '../ProjectForm'; +import CheckCircleOutline from '@mui/icons-material/CheckCircleOutline'; +import UserApiService from '../../../api/UserApiService'; +import ProjectApiService from '../../../api/ProjectApiService'; -const projectApiService = new ProjectApiService(); const userApiService = new UserApiService(); - -const ButtonGroup = ({ btnName1, btnName2, callBackFn1, callBackFn2, isLoading }) => ( - - - callBackFn1(btn)} - > - {isLoading ? : `${btnName1}`} - - - - - {btnName2} - - - -); +const projectApiService = new ProjectApiService(); -const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUsers, editMode, closeConfirmModal, setChangesMade, setCloseConfirmModal, setEditMode, isLoading, setIsLoading }) => { +const ProjectMembersList = ({ projectId, projectMembers, renderedUsers, setRenderedUsers, editMode, closeConfirmModal, setChangesMade, setCloseConfirmModal, setEditMode, isLoading, setEmail, setIsLoading }) => { const [openModal, setOpenModal] = useState(false); const [removeConfirmModal, setRemoveConfirmModal] = useState(false); const [removeId, setRemoveId] = useState(""); const [selectedUserId, setSelectedUserId] = useState(""); // Store user ID state of selected user to show info - console.log('Initial projectMembers:', renderedUsers) - useEffect(() => { setSelectedUserId(""); // close user info when exiting out of "Edit" mode }, [projectMembers, editMode]) @@ -74,7 +41,6 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs if (addedUsers.length === 0 && removedUsers.length === 0) { // No changes made, exit edit mode - console.log('No changes made'); setEditMode(false); setChangesMade(false); return; @@ -129,24 +95,11 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs setChangesMade(false); // Reset changes made setEditMode(false); // Exit edit mode - setIsLoading(false); - - // // Update addedUsers in parallel - // await Promise.all( - // addedUsers.map(userId => - // projectApiService.updateManagedByUsers(projectId, userId, "add") - // ) - // ); - - // // Update removedUsers in parallel - // await Promise.all( - // removedUsers.map(userId => - // projectApiService.updateManagedByUsers(projectId, userId, "remove") - // ) - // ); } catch (err) { console.log(err) + alert('Save failed. Please try again.'); } + setIsLoading(false); } const handleClosePMs = () => setCloseConfirmModal(true); @@ -155,6 +108,7 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs setChangesMade(false); // Discard changes setRenderedUsers(projectMembers); // Reset renderedUsers to original projectMembers setCloseConfirmModal(false); // Close modal and exit edit mode + setEmail(""); // Clear email search field setEditMode(false); } @@ -340,168 +294,4 @@ const ListComponent = ({ projectId, projectMembers, renderedUsers, setRenderedUs ); }; - -const EditProjectMembers = ({ projectToEdit }) => { - // ----------------- States ----------------- - const { auth } = useAuth(); - const [isLoading, setIsLoading] = useState(false); - const [editMode, setEditMode] = useState(false); - const [error, setError] = useState(false); - const [toggleSelect, setToggleSelect] = useState(false); - const [email, setEmail] = useState(''); - const [searchedUser, setSearchedUser] = useState({}); - const [closeConfirmModal, setCloseConfirmModal] = useState(false); - const [changesMade, setChangesMade] = useState(false); - const [projectMembers, setProjectMembers] = useState([]); - const [renderedUsers, setRenderedUsers] = useState([]); - - console.log('changesMade: ', changesMade) - - useEffect(() => { - // Create an array of projectMembers (users) from project's managedByUsers (user IDs) - const fetchProjectMembers = async () => { - if (projectToEdit?.managedByUsers?.length) { - setIsLoading(true); - try { - const members = await Promise.all( - projectToEdit.managedByUsers.map(async (userId) => { - const user = await userApiService.fetchUserById(userId.toString()); - return user; - }) - ); - setProjectMembers(members); - setRenderedUsers(members); - } catch (err) { - console.log(err) - } - setIsLoading(false); - } - } - if (!changesMade) fetchProjectMembers(); - }, [changesMade]); - - const accessLevel = auth?.user?.accessLevel; - const userId = auth?.user?._id; - - // Edit icon component only avaiable for VRMS admins and project members (users in project) - const editIcon = () => { - return (accessLevel !== 'user' || projectToEdit?.managedByUsers?.includes(userId)) && ( - { - if (editMode && changesMade) { - setCloseConfirmModal(true); - } else { - setEditMode(!editMode); - setError(false); - } - }} - > - - - {editMode ? 'Cancel' : 'Edit'} - - - ); - }; - - const handleEmailSearch = async (search) => { - setEmail(search); - // Reset toggleSelect state if user starts typing again - if (toggleSelect) setToggleSelect(false); - - // RegEx for valid email check - const emailRegEx = /^((?:[A-Za-z0-9!#$%&'*+\-\/=?^_`{|}~]|(?<=^|\.)"|"(?=$|\.|@)|(?<=".*)[ .](?=.*")|(? { - setToggleSelect(true); - if (!toggleSelect) { - setRenderedUsers((prevMembers) => [...prevMembers, addedUser]); // Add user to project's managedByUsers array - setChangesMade(true); // Set changes made to true - } - // Confirmation message disappears after 1.5 seconds - setTimeout(() => { - setEmail(""); - setSearchedUser({}); - setToggleSelect(false); - }, 1500); - } - - - return ( - - - {/* Email search componennt */} - - - handleEmailSearch(e.target.value)} - placeholder="Enter user email address" - value={email} - size="small" - /> - - {searchedUser?.email && ( - - - - {!toggleSelect ? searchedUser?.email : User added to project successfully} - - {/* Icons for adding and confirming email of new user */} - {!toggleSelect ? handleAddUser(searchedUser)} /> - : } - - - )} - - {/* Display error message */} - {error && (No account found with this email address)} - {/* Display users */} - - - - ) -} - -export default EditProjectMembers \ No newline at end of file +export default ProjectMembersList \ No newline at end of file diff --git a/client/src/components/manageProjects/editProject.jsx b/client/src/components/manageProjects/editProject.jsx index f4ad6e309..4af063275 100644 --- a/client/src/components/manageProjects/editProject.jsx +++ b/client/src/components/manageProjects/editProject.jsx @@ -10,7 +10,7 @@ import EditIcon from '../../svg/Icon_Edit.svg?react'; import PlusIcon from '../../svg/PlusIcon.svg?react'; import { Typography, Box } from '@mui/material'; -import EditProjectMembers from './editProjectMembers'; +import EditProjectMembers from './editPMs/editProjectMembers'; // Need to hold user state to check which type of user they are and conditionally render editing fields in this component // for user level block access to all except for the ones checked From 7f5e51266649941515b9d2ddc6bb37329b0108fb Mon Sep 17 00:00:00 2001 From: JamesNg Date: Tue, 30 Sep 2025 18:12:56 -0400 Subject: [PATCH 12/12] fix backend issue to convert user id from string to mongoose ObjectId --- backend/controllers/project.controller.js | 18 ++++++++++++++++++ backend/controllers/user.controller.js | 20 +++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/backend/controllers/project.controller.js b/backend/controllers/project.controller.js index 9f7193227..03e710f37 100644 --- a/backend/controllers/project.controller.js +++ b/backend/controllers/project.controller.js @@ -1,4 +1,5 @@ const { Project, User } = require('../models'); +const { ObjectId } = require('mongodb'); const ProjectController = {}; @@ -104,6 +105,23 @@ ProjectController.updateManagedByUsers = async function (req, res) { ProjectController.bulkUpdateManagedByUsers = async function (req, res) { const { bulkOps } = req.body; + + // Convert string IDs to ObjectId in bulkOps + bulkOps.forEach((op) => { + if (op?.updateOne?.filter._id) { + op.updateOne.filter._id = ObjectId(op.updateOne.filter._id); + } + if (op?.updateOne?.update) { + const update = op.updateOne.update; + if (update?.$addToSet?.managedByUsers) { + update.$addToSet.managedByUsers = ObjectId(update.$addToSet.managedByUsers); + } + if (update?.$pull?.managedByUsers) { + update.$pull.managedByUsers = ObjectId(update.$pull.managedByUsers); + } + } + }); + try { const result = await Project.bulkWrite(bulkOps); res.status(200).json(result); diff --git a/backend/controllers/user.controller.js b/backend/controllers/user.controller.js index 0b022327c..70fedc8e0 100644 --- a/backend/controllers/user.controller.js +++ b/backend/controllers/user.controller.js @@ -1,4 +1,5 @@ const jwt = require('jsonwebtoken'); +const { ObjectId } = require('mongodb'); const EmailController = require('./email.controller'); const { CONFIG_AUTH } = require('../config'); @@ -311,7 +312,7 @@ UserController.updateManagedProjects = async function (req, res) { managedByUsers = managedByUsers.filter((id) => id !== UserId); } - // Update user's managedProjects + // Update user's managedProjects user.managedProjects = managedProjects; await user.save({ validateBeforeSave: false }); @@ -328,6 +329,23 @@ UserController.updateManagedProjects = async function (req, res) { UserController.bulkUpdateManagedProjects = async function (req, res) { const { bulkOps } = req.body; + + // Convert string IDs to ObjectId in bulkOps + bulkOps.forEach((op) => { + if (op?.updateOne?.filter._id) { + op.updateOne.filter._id = ObjectId(op.updateOne.filter._id); + } + if (op?.updateOne?.update) { + const update = op.updateOne.update; + if (update?.$addToSet?.managedProjects) { + update.$addToSet.managedProjects = ObjectId(update.$addToSet.managedProjects); + } + if (update?.$pull?.managedProjects) { + update.$pull.managedProjects = ObjectId(update.$pull.managedProjects); + } + } + }); + try { const result = await User.bulkWrite(bulkOps); res.status(200).json(result);