diff --git a/api/src/api/members.js b/api/src/api/members.js index 4feb4c6..5f74b39 100644 --- a/api/src/api/members.js +++ b/api/src/api/members.js @@ -1,6 +1,6 @@ const express = require('express'); const router = express.Router(); -const Member = require('../models/members'); +const { Member, chapterEnum } = require('../models/members'); const errorWrap = require('../middleware/errorWrap'); const { requireRegistered, requireDirector } = require('../middleware/auth'); const { @@ -261,4 +261,31 @@ router.get( }), ); +router.get( + '/role/:role/:chapter?', + requireRegistered, + errorWrap(async (req, res) => { + let members = await Member.find({ role: req.params.role }); + if (req.params.chapter) { + if (!Object.values(chapterEnum).includes(req.params.chapter)){ + return res.status(404).json({ + success: false, + message: req.params.chapter + ' is not a valid chapter.', + }) + } + members = await Member.find({ role: req.params.role, chapter: req.params.chapter }); + } + if (!members) { + return res.status(404).json({ + success: false, + message: req.params.role + ' is not a valid role.', + }) + } + res.json({ + success: true, + result: members, + }); + }), +); + module.exports = router; diff --git a/api/src/models/projects.js b/api/src/models/projects.js index d2d4fda..c100159 100644 --- a/api/src/models/projects.js +++ b/api/src/models/projects.js @@ -46,6 +46,7 @@ const Project = new Schema({ github: String, figma: String, notes: String, + notion: String, }); module.exports = model('Project', Project); diff --git a/client/src/components/table/Table.jsx b/client/src/components/table/Table.jsx index 437ff2b..5345dd3 100644 --- a/client/src/components/table/Table.jsx +++ b/client/src/components/table/Table.jsx @@ -11,10 +11,17 @@ type TableProp = { data: Array, columns: Array, onRowClick: Function, + onRowDoubleClick: Function, sizeToFit?: boolean, }; -const Table = ({ data, columns, onRowClick, sizeToFit }: TableProp): Node => { +const Table = ({ + data, + columns, + onRowClick, + onRowDoubleClick, + sizeToFit, +}: TableProp): Node => { const [entries, setEntries] = useState([]); const [gridApi, setGridApi] = useState(null); @@ -32,7 +39,9 @@ const Table = ({ data, columns, onRowClick, sizeToFit }: TableProp): Node => { return (
- @@ -49,6 +58,7 @@ const Table = ({ data, columns, onRowClick, sizeToFit }: TableProp): Node => { ensureDomOrder: true, }} onRowClicked={(e) => onRowClick?.(e)} + onRowDoubleClicked={(e) => onRowDoubleClick?.(e)} columnDefs={columns} floatingFilter enableCellTextSelection diff --git a/client/src/css/Project.css b/client/src/css/Project.css new file mode 100644 index 0000000..657d461 --- /dev/null +++ b/client/src/css/Project.css @@ -0,0 +1,7 @@ +.ui.form .disabled.field, +field.disabled > label, +.ui.disabled.dropdown, +.ui.form .field.disabled > label { + pointer-events: none; + opacity: 1; +} diff --git a/client/src/pages/Projects.jsx b/client/src/pages/Projects.jsx index f29f6a8..1d27af7 100644 --- a/client/src/pages/Projects.jsx +++ b/client/src/pages/Projects.jsx @@ -2,16 +2,33 @@ import React, { useState, useEffect } from 'react'; import type { Node } from 'react'; import { useHistory } from 'react-router-dom'; +import { isEqual, findKey, partial } from 'lodash'; +import { Sidebar, Segment, Form, Icon, Modal } from 'semantic-ui-react'; import Page from '../components/layout/Page'; import Table from '../components/table/Table'; import { projectColumnDefs } from '../utils/tableHelpers'; -import { getProjects } from '../utils/apiWrapper'; +import { + getProjects, + getMembers, + getUserAuth, + updateProject, +} from '../utils/apiWrapper'; +import { chapterOptions, possibleStatuses } from '../utils/consts'; import '../css/Home.css'; +import '../css/Project.css'; const Projects = (): Node => { const [projects, setProjects] = useState([]); + const [teamMembers, setTeamMembers] = useState([]); + const [visible, setVisible] = useState(false); + const [currProj, setCurrProj] = useState({ projectName: '' }); + const [unmodProj, setUnmodProj] = useState({}); + const [editable, setEditable] = useState(false); + const [editMode, setEditMode] = useState(false); + const [showModal, setShowModal] = useState(false); + const [teamEmails, setTeamEmails] = useState({}); const history = useHistory(); @@ -22,17 +39,219 @@ const Projects = (): Node => { setProjects(allProjects.data.result); } }; + const getAllMembers = async () => { + const allMembers = await getMembers(); + if (allMembers.data) { + const teamMemberList = []; + let teamEmail = {}; + allMembers.data.result.forEach((e) => { + teamEmail = { ...teamEmail, [e.email]: e.firstName + e.lastName }; + teamMemberList.push({ + key: e.firstName + e.lastName, + value: e.firstName + e.lastName, + text: `${e.firstName} ${e.lastName}`, + }); + }); + setTeamMembers(teamMemberList); + setTeamEmails(teamEmail); + } + }; + const getCurrMember = async () => { + const member = await getUserAuth(); + setEditable( + member.data && + (member.data.result.level === 'ADMIN' || + member.data.result.level === 'DIRECTOR'), + ); + }; getAllProjects(); + getAllMembers(); + getCurrMember(); }, []); + const filterObj = (raw, allowed) => { + const filtered = Object.values( + Object.fromEntries( + Object.entries(raw).filter(([key]) => allowed.includes(key)), + ), + ); + return filtered; + }; + return ( - - history.push(`/projects/${e.data._id}`)} + + setShowModal(false), + }, + { + key: 'Save', + content: 'Save', + positive: true, + icon: 'check', + labelPosition: 'right', + onClick: () => { + setUnmodProj(currProj); + updateProject(currProj, currProj._id); + setShowModal(false); + window.location.reload(); + }, + }, + ]} /> - + { + setVisible(false); + setEditMode(false); + if (!isEqual(currProj, unmodProj)) setShowModal(true); + }} + visible={visible} + width="wide" + direction="right" + > + { + setVisible(false); + setEditMode(false); + }} + style={{ cursor: 'pointer' }} + /> + {editable && ( + setEditMode(true)} + /> + )} + + { + setCurrProj({ ...currProj, projectName: value }); + }} + readOnly={!editMode} + /> + { + setCurrProj({ ...currProj, chapter: value }); + }} + disabled={!editMode} + /> + { + setCurrProj({ ...currProj, description: value }); + }} + /> + { + setCurrProj({ ...currProj, status: value }); + }} + disabled={!editMode} + /> + { + setCurrProj({ ...currProj, duration: value }); + }} + /> + { + const newTeam = []; + value.forEach((e) => + newTeam.push(findKey(teamEmails, partial(isEqual, e))), + ); + setCurrProj({ ...currProj, teamMembersEmail: newTeam }); + }} + /> + { + setCurrProj({ ...currProj, github: value }); + }} + /> + { + setCurrProj({ ...currProj, notion: value }); + }} + /> + { + setUnmodProj(currProj); + updateProject(currProj, currProj._id); + window.location.reload(); + }} + /> + + + + + +
history.push(`/projects/${e.data._id}`)} + onRowDoubleClick={(e) => { + setCurrProj(e.data); + setVisible(true); + setUnmodProj(e.data); + }} + /> + + + ); }; diff --git a/client/src/utils/apiWrapper.js b/client/src/utils/apiWrapper.js index 2d1ca02..8b04e77 100644 --- a/client/src/utils/apiWrapper.js +++ b/client/src/utils/apiWrapper.js @@ -230,3 +230,33 @@ export const getProjects = () => { error, })); }; + +// Updates a project +export const updateProject = (project, projectID) => { + const requestString = `${BACKEND_BASE_URL}/projects/${projectID}`; + return axios + .put(requestString, { + ...project, + }) + .catch((error) => ({ + type: 'UPDATE_PROJECT_FAIL', + error, + })); +}; + +export const getRoleCount = (role, chapter) => { + let requestString = `${BACKEND_BASE_URL}/members/role/${role}`; + if (chapter) { + requestString = `${BACKEND_BASE_URL}/members/role/${role}/${chapter}`; + } + return axios + .get(requestString, { + headers: { + 'Content-Type': 'application/JSON', + }, + }) + .catch((error) => ({ + type: 'GET_ROLE_FAIL', + error, + })); +}; diff --git a/client/src/utils/consts.js b/client/src/utils/consts.js index c58e6cf..a604532 100644 --- a/client/src/utils/consts.js +++ b/client/src/utils/consts.js @@ -17,4 +17,95 @@ export const requiredFields = [ 'phone', ]; -export default { levelEnum, requiredFields }; +export const chapterOptions = [ + { + key: 'University of Pennsylvania', + value: 'University of Pennsylvania', + text: 'University of Pennsylvania', + }, + { + key: 'University of Illinois at Urbana-Champaign', + value: 'University of Illinois at Urbana-Champaign', + text: 'University of Illinois at Urbana-Champaign', + }, + { + key: 'Bits of Good - Georgia Tech', + value: 'Bits of Good - Georgia Tech', + text: 'Bits of Good - Georgia Tech', + }, + { + key: 'Cornell University', + value: 'Cornell University', + text: 'Cornell University', + }, + { + key: 'Boston University', + value: 'Boston University', + text: 'Boston University', + }, + { + key: 'California Polytechnic State University', + value: 'California Polytechnic State University', + text: 'California Polytechnic State University', + }, + { + key: 'McGill University', + value: 'McGill University', + text: 'McGill University', + }, + { + key: 'University of Maryland, College Park', + value: 'University of Maryland, College Park', + text: 'University of Maryland, College Park', + }, + { + key: 'University of Tennessee, Knoxville', + value: 'University of Tennessee, Knoxville', + text: 'University of Tennessee, Knoxville', + }, + { + key: 'University of Michigan', + value: 'University of Michigan', + text: 'University of Michigan', + }, + { + key: 'Carleton College', + value: 'Carleton College', + text: 'Carleton College', + }, + { + key: 'New York University', + value: 'New York University', + text: 'New York University', + }, +]; + +export const possibleStatuses = [ + { + key: 'Product Research', + value: 'Product Research', + text: 'Product Research', + }, + { + key: 'Under Development', + value: 'Under Development', + text: 'Under Development', + }, + { + key: 'On Hold', + value: 'On Hold', + text: 'On Hold', + }, + { + key: 'Completed', + value: 'Completed', + text: 'Completed', + }, + { + key: 'Abandoned', + value: 'Abandoned', + text: 'Abandoned', + }, +]; + +export default { levelEnum, requiredFields, chapterOptions, possibleStatuses }; diff --git a/client/src/utils/tableHelpers.js b/client/src/utils/tableHelpers.js index 08fa2d3..35244ee 100644 --- a/client/src/utils/tableHelpers.js +++ b/client/src/utils/tableHelpers.js @@ -79,6 +79,8 @@ export const memberColumnDefs = Object.freeze([ field: 'name', pinned: 'left', valueGetter: nameGetter, + checkboxSelection: true, + headerCheckboxSelection: true, }, { headerName: 'Class Standing',