diff --git a/backend/modules/email-task/controller.js b/backend/modules/email-task/controller.js index 582d78b1f..90225af30 100644 --- a/backend/modules/email-task/controller.js +++ b/backend/modules/email-task/controller.js @@ -77,10 +77,14 @@ controller.createTravelGrantRejectedTask = async (registration, deliverNow = fal controller.createGenericTask = async (userId, eventId, uniqueId, msgParams, deliverNow = false) => { if (!uniqueId) { + console.log('GENERATING UNIQUE ID'); uniqueId = shortid.generate(); } + console.log('CREATING TASK'); const task = await controller.createTask(userId, eventId, 'generic_' + uniqueId, msgParams); + console.log('CREATED TASK'); if (task && deliverNow) { + console.log('DELIVERING NOW', task); return controller.deliverEmailTask(task); } return task; diff --git a/backend/modules/email-task/routes.js b/backend/modules/email-task/routes.js index 5a8fd15df..55e98c110 100644 --- a/backend/modules/email-task/routes.js +++ b/backend/modules/email-task/routes.js @@ -15,6 +15,7 @@ const sendPreviewEmail = asyncHandler(async (req, res) => { }); const sendBulkEmail = asyncHandler(async (req, res) => { + console.log('BODY', req.body); await EmailTaskController.sendBulkEmail(req.body.recipients, req.body.params, req.event, req.body.uniqueId); return res.status(200).json({}); }); diff --git a/backend/modules/registration/controller.js b/backend/modules/registration/controller.js index 65ae0606b..81b62a8c6 100644 --- a/backend/modules/registration/controller.js +++ b/backend/modules/registration/controller.js @@ -1,5 +1,6 @@ const _ = require('lodash'); const Promise = require('bluebird'); +const mongoose = require('mongoose'); const { RegistrationStatuses, RegistrationFields, FieldTypes } = require('@hackjunction/shared'); const Registration = require('./model'); const { NotFoundError, ForbiddenError } = require('../../common/errors/errors'); @@ -137,7 +138,7 @@ controller.bulkEditRegistrations = (eventId, registrationIds, edits) => { return Registration.updateMany( { event: eventId, - _id: { + user: { $in: registrationIds } }, @@ -177,13 +178,16 @@ controller.rejectPendingTravelGrants = eventId => { }; controller.getFullRegistration = (eventId, registrationId) => { - return Registration.findById(registrationId).then(registration => { - if (!registration || registration.event.toString() !== eventId) { - throw new NotFoundError(`Registration with id ${registrationId} does not exist`); - } + const query = mongoose.Types.ObjectId.isValid(registrationId) ? { _id: registrationId } : { user: registrationId }; + return Registration.findOne(query) + .and({ event: eventId }) + .then(registration => { + if (!registration) { + throw new NotFoundError(`Registration with id ${registrationId} does not exist`); + } - return registration; - }); + return registration; + }); }; controller.editRegistration = (registrationId, event, data, user) => { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 22e4dd8f6..01a953ad0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1057,16 +1057,6 @@ "requires": { "lodash": "^4.17.15", "object-path": "^0.11.4" - }, - "dependencies": { - "lodash": { - "version": "4.17.15", - "bundled": true - }, - "object-path": { - "version": "0.11.4", - "bundled": true - } } }, "@hapi/address": { @@ -3630,7 +3620,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -3651,12 +3642,14 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3671,17 +3664,20 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3798,7 +3794,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", @@ -3810,6 +3807,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3824,6 +3822,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3831,12 +3830,14 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3855,6 +3856,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3935,7 +3937,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3947,6 +3950,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } @@ -4032,7 +4036,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4068,6 +4073,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4087,6 +4093,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4130,12 +4137,14 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "optional": true } } }, @@ -8906,7 +8915,8 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true }, "aproba": { "version": "1.2.0", @@ -8927,12 +8937,14 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "optional": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -8947,17 +8959,20 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "optional": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "optional": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -9074,7 +9089,8 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "optional": true }, "ini": { "version": "1.3.5", @@ -9086,6 +9102,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9100,6 +9117,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9107,12 +9125,14 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "optional": true }, "minipass": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9131,6 +9151,7 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "optional": true, "requires": { "minimist": "0.0.8" } @@ -9211,7 +9232,8 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "optional": true }, "object-assign": { "version": "4.1.1", @@ -9223,6 +9245,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "optional": true, "requires": { "wrappy": "1" } @@ -9308,7 +9331,8 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -9344,6 +9368,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9363,6 +9388,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -9406,12 +9432,14 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "optional": true }, "yallist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", + "optional": true } } } diff --git a/frontend/package.json b/frontend/package.json index f0c161f52..2e2583eb5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,7 +25,7 @@ "node-sass": "^4.11.0", "notistack": "^0.9.2", "object-path": "^0.11.4", - "react": "^16.8.1", + "react": "^16.9.0", "react-animate-height": "^2.0.15", "react-app-rewired": "^2.1.3", "react-dom": "^16.8.1", diff --git a/frontend/src/components/generic/ConfirmDialog/index.js b/frontend/src/components/generic/ConfirmDialog/index.js new file mode 100644 index 000000000..4742c195e --- /dev/null +++ b/frontend/src/components/generic/ConfirmDialog/index.js @@ -0,0 +1,41 @@ +import React, { useCallback } from 'react'; + +import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@material-ui/core'; + +const ConfirmDialog = ({ + open, + onClose = () => {}, + onCancel = () => {}, + onOk = () => {}, + title, + message, + cancelText = 'Cancel', + okText = 'OK' +}) => { + const handleCancel = useCallback(() => { + onClose(); + onCancel(); + }, [onClose, onCancel]); + + const handleOk = useCallback(() => { + onClose(); + onOk(); + }, [onClose, onOk]); + + return ( + + {title} + + {message} + + + {cancelText} + + {okText} + + + + ); +}; + +export default ConfirmDialog; diff --git a/frontend/src/components/generic/MaterialTable/index.js b/frontend/src/components/generic/MaterialTable/index.js index 286f927b1..99cd7871d 100644 --- a/frontend/src/components/generic/MaterialTable/index.js +++ b/frontend/src/components/generic/MaterialTable/index.js @@ -44,7 +44,11 @@ const _MaterialTable = props => { {props.title}} + title={ + {`${props.title} ${ + props.showCount ? '(' + props.data.length + ')' : '' + }`} + } /> ); }; diff --git a/frontend/src/components/generic/Modal/index.js b/frontend/src/components/generic/Modal/index.js index 6116e989e..95decddf4 100644 --- a/frontend/src/components/generic/Modal/index.js +++ b/frontend/src/components/generic/Modal/index.js @@ -32,7 +32,7 @@ const useStyles = makeStyles(theme => ({ }, header: { padding: theme.spacing(3), - textAlign: 'left' + textAlign: 'center' }, inner: { padding: '1rem', @@ -41,7 +41,7 @@ const useStyles = makeStyles(theme => ({ } })); -const GenericModal = ({ title, isOpen, handleClose, size, children }) => { +const GenericModal = ({ title, isOpen, handleClose, size, children, footer = null }) => { const classes = useStyles(); return ( { > {title && ( - {title} + {title} )} {children} + {footer} ); }; diff --git a/frontend/src/components/generic/UserListItem/OrganiserListItem.js b/frontend/src/components/generic/UserListItem/OrganiserListItem.js new file mode 100644 index 000000000..51e2e49f9 --- /dev/null +++ b/frontend/src/components/generic/UserListItem/OrganiserListItem.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; +import UserListItem from './index'; + +const OrganiserListItem = ({ userId, organisersMap = {} }) => { + return ; +}; + +const mapState = state => ({ + organisersMap: OrganiserSelectors.organisersMap(state) +}); + +export default connect(mapState)(OrganiserListItem); diff --git a/frontend/src/components/generic/UserListItem/index.js b/frontend/src/components/generic/UserListItem/index.js new file mode 100644 index 000000000..0e97244af --- /dev/null +++ b/frontend/src/components/generic/UserListItem/index.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { ListItem, ListItemAvatar, ListItemText, Avatar } from '@material-ui/core'; + +const UserListItem = ({ user, selectable = false, selected = false, onSelect = () => {} }) => { + const userName = user ? `${user.firstName} ${user.lastName}` : ''; + const userEmail = user ? user.email : ''; + + return ( + + {user ? ( + + + + + + + ) : ( + + )} + + ); +}; + +export default UserListItem; diff --git a/frontend/src/components/inputs/TextInput/index.js b/frontend/src/components/inputs/TextInput/index.js index 0664411a9..897fc8004 100644 --- a/frontend/src/components/inputs/TextInput/index.js +++ b/frontend/src/components/inputs/TextInput/index.js @@ -2,7 +2,7 @@ import React, { useCallback } from 'react'; import { TextField } from '@material-ui/core'; -const TextInput = ({ label, helperText, value = '', onChange = () => {}, error, disabled, rawOnChange = false, type = 'text', multiline = false, formatValue, formatOnChange }) => { +const TextInput = ({ label, helperText, value = '', onChange = () => {}, error, disabled, rawOnChange = false, type = 'text', textarea = false, formatValue, formatOnChange }) => { const handleChange = useCallback( e => { if (rawOnChange) { @@ -13,12 +13,25 @@ const TextInput = ({ label, helperText, value = '', onChange = () => {}, error, onChange(val); } }, - [onChange, rawOnChange] + [onChange, rawOnChange, formatOnChange] ); const formattedValue = formatValue ? formatValue(value) : value; - return ; + return( + + ); }; export default TextInput; diff --git a/frontend/src/components/modals/BulkEditRegistrationModal/index.js b/frontend/src/components/modals/BulkEditRegistrationModal/index.js index 33672bf8a..282075cdd 100644 --- a/frontend/src/components/modals/BulkEditRegistrationModal/index.js +++ b/frontend/src/components/modals/BulkEditRegistrationModal/index.js @@ -3,76 +3,114 @@ import { connect } from 'react-redux'; import Modal from 'components/generic/Modal'; import { withSnackbar } from 'notistack'; -import { Typography, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Button } from '@material-ui/core'; +import { + Box, + Typography, + ExpansionPanel, + ExpansionPanelSummary, + ExpansionPanelDetails, + Button +} from '@material-ui/core'; import Rating from '@material-ui/lab/Rating'; import PageWrapper from 'components/PageWrapper'; import CenteredContainer from 'components/generic/CenteredContainer'; import PageHeader from 'components/generic/PageHeader'; -import UserSelectModal from 'components/modals/UserSelectModal'; +import OrganiserSelectModal from 'components/modals/OrganiserSelectModal'; +import OrganiserListItem from 'components/generic/UserListItem/OrganiserListItem'; +import EventTagsSelect from 'components/FormComponents/EventTagsSelect'; +import RegistrationStatusSelect from 'components/FormComponents/RegistrationStatusSelect'; +import ConfirmDialog from 'components/generic/ConfirmDialog'; import * as AuthSelectors from 'redux/auth/selectors'; import * as OrganiserSelectors from 'redux/organiser/selectors'; import * as OrganiserActions from 'redux/organiser/actions'; import { useFormField } from 'hooks/formHooks'; -const BulkEditRegistrationModal = ({ registrationIds = [], onClose, organisers }) => { +const BulkEditRegistrationModal = ({ + visible, + registrationIds = [], + onClose, + onSubmit, + organisers, + event, + enqueueSnackbar +}) => { const [loading, setLoading] = useState(false); - const rating = useFormField(0); + const [organiserModal, setOrganiserModal] = useState(false); + const [confirmDialog, setConfirmDialog] = useState(false); + const rating = useFormField(null); const assignedTo = useFormField(null); - // const [error, setError] = useState(false); - // const [registration, setRegistration] = useState(); - // const { slug } = event; + const tags = useFormField([]); + const status = useFormField('pending'); - // useEffect(() => { - // if (registrationId) { - // setLoading(true); - // RegistrationsService.getFullRegistration(idToken, slug, registrationId) - // .then(data => { - // setRegistration(data); - // }) - // .catch(err => { - // setError(true); - // }) - // .finally(() => { - // setLoading(false); - // }); - // } - // }, [idToken, registrationId, slug]); + const [expandedIds, setExpandedIds] = useState([]); - // const participantName = useMemo(() => { - // if (!registration) return ''; - // const { firstName, lastName } = registration.answers; - // return `${firstName} ${lastName}`; - // }, [registration]); + const isExpanded = useCallback( + panel => { + return expandedIds.indexOf(panel) !== -1; + }, + [expandedIds] + ); + + const toggleExpanded = panel => { + if (isExpanded(panel)) { + setExpandedIds(expandedIds.filter(id => id !== panel)); + } else { + setExpandedIds(expandedIds.concat(panel)); + } + }; + + const reset = useCallback(() => { + rating.reset(); + assignedTo.reset(); + tags.reset(); + status.reset(); + setLoading(false); + setExpandedIds([]); + }, [rating, assignedTo, tags, status]); + + const handleClose = useCallback(() => { + reset(); + onClose(); + }, [reset, onClose]); + + const getEdits = useCallback(() => { + const edits = {}; + if (isExpanded('rating')) edits.rating = rating.value; + if (isExpanded('assignedTo')) edits.assignedTo = assignedTo.value; + if (isExpanded('tags')) edits.tags = tags.value; + if (isExpanded('status')) edits.status = status.value; + return edits; + }, [rating, assignedTo, tags, status, isExpanded]); - // const participantSubheading = useMemo(() => { - // if (!registration) return ''; - // return registration.answers.countryOfResidence; - // }, [registration]); + const handleSubmit = useCallback(() => { + setLoading(true); + const edits = getEdits(); - // const handleEdit = useCallback( - // async data => { - // setLoading(true); - // await MiscUtils.sleep(1000); - // editRegistration(registrationId, data, slug) - // .then(data => { - // enqueueSnackbar('Changes saved!', { variant: 'success' }); - // onEdited(data); - // onClose(); - // }) - // .catch(err => { - // enqueueSnackbar('Something went wrong', { variant: 'error' }); - // }) - // .finally(() => { - // setLoading(false); - // }); - // }, - // [enqueueSnackbar, editRegistration, registrationId, slug, onClose, onEdited] - // ); + onSubmit(registrationIds, edits, event.slug) + .then(() => { + enqueueSnackbar(`Edited ${registrationIds.length} registrations`, { variant: 'success' }); + }) + .catch(err => { + enqueueSnackbar('Something went wrong', { variant: 'error' }); + }) + .finally(() => { + setLoading(false); + handleClose(); + }); + }, [onSubmit, handleClose, getEdits, event.slug, registrationIds, enqueueSnackbar]); + if (!registrationIds.length) return null; return ( - + + setConfirmDialog(false)} + onOk={handleSubmit} + /> @@ -80,95 +118,110 @@ const BulkEditRegistrationModal = ({ registrationIds = [], onClose, organisers } want to edit - if a panel is left un-expanded, that field will not be edited in the registrations. - + toggleExpanded('rating')}> - Rating + + Rating + {isExpanded('rating') ? ( + + {rating.value ? 'Set rating to ' + rating.value : 'Clear rating'} + + ) : ( + + Leave unchanged + + )} + - + rating.setValue(num)} /> - + toggleExpanded('assignedTo')}> - Assigned to + + Assigned to + {isExpanded('assignedTo') ? ( + + {assignedTo.value ? 'Change assigned to' : 'Clear assigned to'} + + ) : ( + + Leave unchanged + + )} + - ( - - {assignedTo.value} - - Change - - {assignedTo && ( - - Clear - - )} - - )} - onDone={value => assignedTo.setValue(value.userId)} - allowMultiple={false} - userProfiles={organisers} + assignedTo.setValue(value.userId)} + onClear={assignedTo.setValue} /> - Details here + + + + + setOrganiserModal(true)}> + Change + + - + toggleExpanded('tags')}> - Tags + + Tags + {isExpanded('tags') ? ( + + {tags.value && tags.value.length + ? 'Set tags to ' + tags.value.join(', ') + : 'Clear tags'} + + ) : ( + + Leave unchanged + + )} + - Details here + - + toggleExpanded('status')}> - Status + + Status + {isExpanded('status') ? ( + + Set status to {status.value} + + ) : ( + + Leave unchanged + + )} + - Details here + + + setConfirmDialog(true)} + variant="contained" + color="primary" + disabled={expandedIds.length === 0} + > + {expandedIds.length === 0 + ? 'Expand panels to make edits' + : ` Apply edits to ${registrationIds.length} registrations`} + + - {/* - - ( - - {renderAssignedTo()} - - Change - - {assignedTo && ( - setAssignedTo(null)}> - Clear - - )} - - )} - onDone={handleAssignedChange} - allowMultiple={false} - userProfiles={organisers} - /> - - - - - - - - - - {renderPreview()} - setVisible(false) }} /> - - */} ); @@ -182,8 +235,8 @@ const mapState = state => ({ }); const mapDispatch = dispatch => ({ - editRegistration: (registrationId, data, slug) => - dispatch(OrganiserActions.editRegistration(registrationId, data, slug)) + onSubmit: (registrationIds, edits, slug) => + dispatch(OrganiserActions.bulkEditRegistrations(registrationIds, edits, slug)) }); export default withSnackbar( diff --git a/frontend/src/components/modals/BulkEmailModal/index.js b/frontend/src/components/modals/BulkEmailModal/index.js new file mode 100644 index 000000000..dc6da125c --- /dev/null +++ b/frontend/src/components/modals/BulkEmailModal/index.js @@ -0,0 +1,320 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { connect } from 'react-redux'; +import Modal from 'components/generic/Modal'; +import { withSnackbar } from 'notistack'; + +import { Typography, Grid, Box, Button } from '@material-ui/core'; +import PageWrapper from 'components/PageWrapper'; +import CenteredContainer from 'components/generic/CenteredContainer'; +import PageHeader from 'components/generic/PageHeader'; +import TextInput from 'components/inputs/TextInput'; +import ConfirmDialog from 'components/generic/ConfirmDialog'; + +import * as AuthSelectors from 'redux/auth/selectors'; +import * as UserSelectors from 'redux/user/selectors'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; +import * as OrganiserActions from 'redux/organiser/actions'; +import { useFormField } from 'hooks/formHooks'; +import EmailService from 'services/email'; + +const BulkEmailModal = ({ + visible, + registrationIds = [], + onClose, + organisers, + idToken, + event, + user, + enqueueSnackbar +}) => { + const [loading, setLoading] = useState(false); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); + const headerImage = useFormField(''); + const subject = useFormField('', value => { + if (!value || value.length === 0) { + return 'Message subject is required!'; + } + if (value.length > 50) { + return 'Message subject can be at most 50 characters'; + } + + return; + }); + const subtitle = useFormField(''); + const body = useFormField('', value => { + if (!body || body.length === 0) { + return 'Message body is required!'; + } + + if (body.length > 1000) { + return 'Message body can be at most 1000 characters'; + } + + return; + }); + const messageId = useFormField(''); + const ctaText = useFormField(''); + const ctaLink = useFormField( + '', + useCallback( + value => { + if (ctaText.value && ctaText.value.length > 0) { + if (!value || value.length === 0) { + return 'Call to action link is required, if call to action title is entered'; + } + if (value.indexOf('http') !== 0) { + return 'Call to action link must be a valid url, starting with http...'; + } + } + return; + }, + [ctaText.value] + ) + ); + + const fields = [headerImage, subject, subtitle, body, messageId, ctaText, ctaLink]; + + const params = { + subject: subject.value, + subtitle: subtitle.value, + header_image: headerImage.value, + body: body.value, + cta_text: ctaText.value, + cta_link: ctaLink.value + }; + + const validate = useCallback(() => { + const errors = fields + .map(field => { + return field.validate(); + }) + .filter(error => typeof error !== 'undefined'); + + if (errors.length > 0) { + return false; + } + return true; + }, [fields]); + + const handleTestEmail = useCallback(() => { + if (!validate()) return; + setLoading(true); + EmailService.sendPreviewEmail(idToken, event.slug, user.email, params) + .then(() => { + enqueueSnackbar('Test email sent to ' + user.email, { variant: 'success' }); + }) + .catch(err => { + console.log(err); + enqueueSnackbar('Something went wrong', { variant: 'success' }); + }) + .finally(() => { + setLoading(false); + }); + return null; + }, [idToken, event.slug, user.email, params, enqueueSnackbar, validate]); + + const handleConfirm = useCallback(() => { + if (!validate()) return; + setLoading(true); + EmailService.sendBulkEmail(idToken, event.slug, registrationIds, params, messageId.value) + .then(() => { + enqueueSnackbar('Email sent to ' + registrationIds.length + ' recipients', { variant: 'success' }); + }) + .catch(err => { + console.log(err); + enqueueSnackbar('Something went wrong', { variant: 'error' }); + }) + .finally(() => { + setLoading(false); + onClose(); + }); + }, [idToken, event.slug, params, registrationIds, messageId, enqueueSnackbar, validate, onClose]); + + if (!registrationIds.length) return null; + + return ( + + + + + + + Here you can send an email to all selected participants. Type in your own email address below to + test the email before sending it out to everyone! + + + + + + + + + + + + + + + + + + + + + + + + + + + Send to yourself + + + + Send to {registrationIds.length} recipients + + + + + {/* + Message body + setBody(e.target.value)} + autosize={{ + minRows: 10, + maxRows: 20 + }} + > + The content of your email + + Unique message id + setMessageId(e.target.value)} + size="large" + placeholder="something-you-will-remember" + /> + + If you want, you can enter a unique message id here. Messages with the same id will only be sent + once to a given participant - this is useful if you want to avoid sending out the same message to a + participant who has already received it earlier. + + + Call to action + setCtaText(e.target.value)} + size="large" + placeholder="Click this button" + /> + + If your want a Call to Action button in your email, enter the text for the button here. + + + Call to action link + setCtaLink(e.target.value)} + size="large" + placeholder="https://..." + /> + Enter the link where your Call to Action button should lead + + + Test email + + + + + Send to {registrationIds.length} recipients + + */} + + + + ); +}; + +const mapState = state => ({ + idToken: AuthSelectors.getIdToken(state), + user: UserSelectors.userProfile(state), + event: OrganiserSelectors.event(state), + organisersMap: OrganiserSelectors.organisersMap(state), + organisers: OrganiserSelectors.organisers(state) +}); + +const mapDispatch = dispatch => ({ + editRegistration: (registrationId, data, slug) => + dispatch(OrganiserActions.editRegistration(registrationId, data, slug)) +}); + +export default withSnackbar( + connect( + mapState, + mapDispatch + )(BulkEmailModal) +); diff --git a/frontend/src/components/modals/OrganiserSelectModal/index.js b/frontend/src/components/modals/OrganiserSelectModal/index.js new file mode 100644 index 000000000..7dd3ef2ae --- /dev/null +++ b/frontend/src/components/modals/OrganiserSelectModal/index.js @@ -0,0 +1,56 @@ +import React, { useState, useCallback } from 'react'; +import { connect } from 'react-redux'; + +import { Box, List, Button } from '@material-ui/core'; + +import Modal from 'components/generic/Modal'; +import * as OrganiserSelectors from 'redux/organiser/selectors'; + +import UserListItem from 'components/generic/UserListItem'; + +const OrganiserSelectModal = ({ open, onClose, onClear, onSelect, organisers }) => { + const [selected, setSelected] = useState(); + const handleClear = useCallback(() => { + onClose(); + onClear(); + }, [onClose, onClear]); + const handleSubmit = useCallback(() => { + onClose(); + onSelect(selected); + }, [selected, onClose, onSelect]); + return ( + + + Clear selection + + + + {selected ? `${selected.firstName} ${selected.lastName}` : 'Select a user'} + + + } + > + + {organisers.map(organiser => ( + setSelected(organiser)} + selected={selected && selected.userId === organiser.userId} + key={organisers.userId} + user={organiser} + /> + ))} + + + ); +}; + +const mapState = state => ({ + organisers: OrganiserSelectors.organisers(state) +}); +export default connect(mapState)(OrganiserSelectModal); diff --git a/frontend/src/components/tables/AttendeeTable/index.js b/frontend/src/components/tables/AttendeeTable/index.js index 0ddc413a2..618b2a52d 100644 --- a/frontend/src/components/tables/AttendeeTable/index.js +++ b/frontend/src/components/tables/AttendeeTable/index.js @@ -7,11 +7,13 @@ import { RegistrationStatuses } from '@hackjunction/shared'; import EmailIcon from '@material-ui/icons/Email'; import EditIcon from '@material-ui/icons/Edit'; +import { Box, Paper } from '@material-ui/core'; import MaterialTable from 'components/generic/MaterialTable'; import * as OrganiserSelectors from 'redux/organiser/selectors'; import EditRegistrationModal from 'components/modals/EditRegistrationModal'; import BulkEditRegistrationModal from 'components/modals/BulkEditRegistrationModal'; +import BulkEmailModal from 'components/modals/BulkEmailModal'; const AttendeeTable = ({ organiserProfilesMap, @@ -20,7 +22,8 @@ const AttendeeTable = ({ loading, attendees = [], footer = null, - title = 'Participants' + title = 'Participants', + minimal = false }) => { const [editing, setEditing] = useState(); const [selected, setSelected] = useState([]); @@ -43,27 +46,35 @@ const AttendeeTable = ({ return ( setEditing(row._id)} - onSelectionChange={rows => setSelected(rows.map(r => r._id))} - actions={[ - { - icon: forwardRef((props, ref) => ), - tooltip: 'Email selected', - onClick: toggleBulkEmail - }, - { - icon: forwardRef((props, ref) => ), - tooltip: 'Edit selected', - onClick: toggleBulkEdit - } - ]} + onRowClick={(e, row) => setEditing(row.user)} + onSelectionChange={rows => setSelected(rows.map(r => r.user))} + actions={ + !minimal + ? [ + { + icon: forwardRef((props, ref) => ), + tooltip: 'Email selected', + onClick: toggleBulkEmail + }, + { + icon: forwardRef((props, ref) => ), + tooltip: 'Edit selected', + onClick: toggleBulkEdit + } + ] + : [] + } options={{ - exportButton: true, - selection: true, - showSelectAllCheckbox: true, - pageSizeOptions: [5, 25, 50] + exportButton: !minimal, + selection: !minimal, + showSelectAllCheckbox: !minimal, + pageSizeOptions: [5, 25, 50], + debounceInterval: 500, + search: !minimal, + paging: !minimal }} localization={{ toolbar: { @@ -71,6 +82,11 @@ const AttendeeTable = ({ nRowsSelected: '{0} selected' } }} + components={{ + Container: forwardRef((props, ref) => + minimal ? : + ) + }} columns={[ { title: 'First name', @@ -85,7 +101,8 @@ const AttendeeTable = ({ { title: 'Email', field: 'answers.email', - searchable: true + searchable: true, + hidden: minimal }, { title: 'Rating', @@ -130,6 +147,7 @@ const AttendeeTable = ({ { title: 'Assigned to', field: 'assignedTo', + hidden: minimal, render: row => { const userId = row.assignedTo; let text; @@ -159,11 +177,8 @@ const AttendeeTable = ({ return ( - + + {renderTable()} {renderEmpty()} diff --git a/frontend/src/components/tables/TeamsTable/index.js b/frontend/src/components/tables/TeamsTable/index.js new file mode 100644 index 000000000..c2f3324f5 --- /dev/null +++ b/frontend/src/components/tables/TeamsTable/index.js @@ -0,0 +1,287 @@ +import React, { useMemo, useState, useCallback, forwardRef } from 'react'; + +import { connect } from 'react-redux'; +import { sumBy } from 'lodash-es'; +import { makeStyles } from '@material-ui/core/styles'; +import { Typography, Grid, FormControlLabel, Switch, FormGroup, Box, Slider, Paper } from '@material-ui/core'; +import EmailIcon from '@material-ui/icons/Email'; +import EditIcon from '@material-ui/icons/Edit'; + +import * as OrganiserSelectors from 'redux/organiser/selectors'; +import MaterialTable from 'components/generic/MaterialTable'; +import AttendeeTable from 'components/tables/AttendeeTable'; +import Select from 'components/inputs/Select'; +import BulkEditRegistrationModal from 'components/modals/BulkEditRegistrationModal'; +import BulkEmailModal from 'components/modals/BulkEmailModal'; + +const useStyles = makeStyles(theme => ({ + detailPanel: { + padding: theme.spacing(2), + backgroundColor: '#fafafa' + } +})); + +const TeamsTable = ({ loading, teams = [], registrationsMap }) => { + const classes = useStyles(); + const [reviewStatus, setReviewStatus] = useState('any'); + const [lockedStatus, setLockedStatus] = useState('any'); + const [ratingRange, setRatingRange] = useState([0, 5]); + const [bulkEdit, setBulkEdit] = useState(false); + const [bulkEmail, setBulkEmail] = useState(false); + const [searchActive, setSearchActive] = useState(false); + + const handleSearchChange = useCallback(val => { + if (val && val.length > 0) { + setSearchActive(true); + } else { + setSearchActive(false); + } + }, []); + + const handleRatingRangeChange = useCallback((e, value) => { + setRatingRange(value); + }, []); + + const teamsPopulated = useMemo(() => { + return teams.map(team => { + const membersMapped = team.members + .map(member => { + return registrationsMap[member]; + }) + .filter(member => typeof member !== 'undefined'); + const ownerMapped = registrationsMap[team.owner] || {}; + const allMembers = membersMapped.concat(ownerMapped); + const reviewedCount = allMembers.filter(member => member && member.rating).length; + const memberCount = allMembers.length; + return { + ...team, + owner: ownerMapped, + members: allMembers, + avgRating: (sumBy(allMembers, m => m.rating || 0) / allMembers.length).toFixed(2), + reviewedPercent: Math.floor((reviewedCount * 100) / memberCount) + }; + }); + }, [teams, registrationsMap]); + + const teamsFiltered = teamsPopulated.filter(team => { + if (lockedStatus === 'locked' && !team.locked) { + return false; + } + if (lockedStatus === 'not-locked' && team.locked) { + return false; + } + if (reviewStatus === 'fully-reviewed' && team.reviewedPercent !== 100) { + return false; + } + + if (reviewStatus === 'not-reviewed' && team.reviewedPercent === 100) { + return false; + } + + if (ratingRange[0] > team.avgRating) { + return false; + } + + if (ratingRange[1] < team.avgRating) { + return false; + } + return true; + }); + + const filteredMemberIds = useMemo(() => { + return teamsFiltered.reduce((res, team) => { + return res.concat(team.members.map(reg => reg.user)); + }, []); + }, [teamsFiltered]); + + return ( + + + + + + + + + + + + + + + + + + + + + + Rating between + + + + + + + + + ), + tooltip: 'Email all', + isFreeAction: true, + onClick: () => setBulkEmail(true), + hidden: searchActive + }, + { + icon: forwardRef((props, ref) => ), + tooltip: 'Edit all', + isFreeAction: true, + onClick: () => setBulkEdit(true), + hidden: searchActive + } + ]} + localization={{ + toolbar: { + searchPlaceholder: 'Search by code / owner' + } + }} + options={{ + debounceInterval: 500, + pageSizeOptions: [5, 25, 50] + }} + detailPanel={rowData => { + return ( + + + + ); + }} + columns={[ + { + title: 'Owner', + field: 'owner', + searchable: true, + customFilterAndSearch: (keyword, row, { render }) => { + return render(row).indexOf(keyword) !== -1; + }, + render: row => { + const { owner } = row; + if (!owner || !owner.answers) return '???'; + return `${owner.answers.firstName} ${owner.answers.lastName}`; + } + }, + { + title: 'Code', + field: 'code', + searchable: true + }, + { + title: 'Members', + field: 'members', + render: row => row.members.length + }, + { + title: 'Avg. Rating', + field: 'avgRating' + }, + { + title: '% Reviewed', + field: 'reviewedPercent', + render: row => { + if (row.reviewedPercent === 100) { + return ( + + 100% + + ); + } else { + return ( + + {row.reviewedPercent}% + + ); + } + } + }, + { + title: 'Locked', + field: 'locked', + render: row => { + if (row.locked) { + return ( + + Yes + + ); + } else { + return ( + + No + + ); + } + } + } + ]} + /> + + + ); +}; + +const mapState = state => ({ + registrationsMap: OrganiserSelectors.registrationsMap(state) +}); + +export default connect(mapState)(TeamsTable); diff --git a/frontend/src/hooks/customHooks.js b/frontend/src/hooks/customHooks.js index c670fcc3a..39a65db69 100644 --- a/frontend/src/hooks/customHooks.js +++ b/frontend/src/hooks/customHooks.js @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; export const useStateWithReset = initialValue => { const [value, setValue] = useState(initialValue); @@ -6,3 +6,12 @@ export const useStateWithReset = initialValue => { return [value, setValue, resetValue]; }; + +export const useToggle = initialValue => { + const [value, setValue] = useState(initialValue); + const toggleValue = useCallback(() => { + setValue(!value); + }, [value]); + + return [value, toggleValue]; +}; diff --git a/frontend/src/hooks/formHooks.js b/frontend/src/hooks/formHooks.js index 7e2e0d629..2976418b2 100644 --- a/frontend/src/hooks/formHooks.js +++ b/frontend/src/hooks/formHooks.js @@ -22,9 +22,9 @@ export const useFormField = (initialValue, validate = () => null, initialError = ); const reset = useCallback(() => { - setValue(undefined); + setValue(initialValue); setError(undefined); - }, []); + }, [initialValue]); const handleValidate = useCallback(() => { const err = validate(value); diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/AssignAttendeesPage.module.scss b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/AssignAttendeesPage.module.scss deleted file mode 100644 index df0249912..000000000 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/AssignAttendeesPage.module.scss +++ /dev/null @@ -1,28 +0,0 @@ -.top { - display: flex; - flex-direction: row; - justify-content: space-between; - padding: 1rem; - - .title { - font-weight: bold; - } -} - -.topLeft { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: flex-end; -} - -.toggle { - display: flex; - flex-direction: row; - align-items: center; - padding: 0 0.5rem; -} - -.toggleText { - padding-right: 1rem; -} diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/index.js index 2e010f8f6..2fc389081 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/index.js +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Assigned/index.js @@ -1,46 +1,45 @@ import React, { useState, useMemo } from 'react'; -import styles from './AssignAttendeesPage.module.scss'; -import { Button as AntButton, Modal, message, Switch } from 'antd'; import { connect } from 'react-redux'; +import { Switch, Button, Typography, Grid, Box } from '@material-ui/core'; +import { withSnackbar } from 'notistack'; -import Divider from 'components/generic/Divider'; import AttendeeTable from 'components/tables/AttendeeTable'; - +import ConfirmDialog from 'components/generic/ConfirmDialog'; import * as OrganiserSelectors from 'redux/organiser/selectors'; import * as AuthSelectors from 'redux/auth/selectors'; import * as OrganiserActions from 'redux/organiser/actions'; +import { useToggle } from 'hooks/customHooks'; + import RegistrationsService from 'services/registrations'; -import BulkEditRegistrationDrawer from 'components/modals/BulkEditRegistrationDrawer'; -const SearchAttendeesPage = ({ idToken, event, registrations = [], registrationsLoading, updateRegistrations }) => { +const SearchAttendeesPage = ({ + idToken, + event, + registrations = [], + registrationsLoading, + updateRegistrations, + enqueueSnackbar +}) => { const [hideRated, setHideRated] = useState(false); + const [confirmModal, toggleConfirmModal] = useToggle(false); const { slug } = event; const handleSelfAssign = () => { - Modal.confirm({ - title: 'Please read this first :)', - content: - "This means 10 random, un-rated registrations will be assigned to you, and won't be shown to other reviewers. Please make sure you review all of the registrations assigned to you.", - onOk() { - const hideMessage = message.loading('Assigning random registrations', 0); - RegistrationsService.assignRandomRegistrations(idToken, slug) - .then(data => { - if (data === 0) { - message.success('No available registrations to assign!'); - } else { - message.success('Done! Assigned ' + data + ' registrations to you'); - } - }) - .catch(() => { - message.error("Oops, something wen't wrong..."); - }) - .finally(() => { - updateRegistrations(slug); - hideMessage(); - }); - } - }); + RegistrationsService.assignRandomRegistrations(idToken, slug) + .then(data => { + if (data === 0) { + enqueueSnackbar('No available registrations to assign!', { variant: 'success' }); + } else { + enqueueSnackbar('Assigned ' + data + ' registrations to you', { variant: 'success' }); + } + }) + .catch(() => { + enqueueSnackbar('Something went wrong...'); + }) + .finally(() => { + updateRegistrations(slug); + }); }; const filtered = useMemo(() => { @@ -53,24 +52,27 @@ const SearchAttendeesPage = ({ idToken, event, registrations = [], registrations }, [registrations, hideRated]); return ( - - - {filtered.length} registrations - - - Hide rated registrations - - - + + + + + Hide rated registrations + setHideRated(e.target.checked)} /> + Assign random registrations - - - {/* r._id)} /> */} - - - - - + + + + + + + ); }; @@ -85,7 +87,9 @@ const mapDispatch = dispatch => ({ updateRegistrations: slug => dispatch(OrganiserActions.updateRegistrationsForEvent(slug)) }); -export default connect( - mapState, - mapDispatch -)(SearchAttendeesPage); +export default withSnackbar( + connect( + mapState, + mapDispatch + )(SearchAttendeesPage) +); diff --git a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Teams/index.js b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Teams/index.js index 2ad347a3c..0b2fc291f 100644 --- a/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Teams/index.js +++ b/frontend/src/pages/OrganiserDashboard/OrganiserEditEvent/Participants/Teams/index.js @@ -1,175 +1,23 @@ -import React, { useMemo, useState } from 'react'; -import styles from './TeamsPage.module.scss'; +import React from 'react'; import { connect } from 'react-redux'; -import { Tag, Table, Switch, Input } from 'antd'; -import { sumBy } from 'lodash-es'; import * as OrganiserSelectors from 'redux/organiser/selectors'; import PageWrapper from 'components/PageWrapper'; -import AttendeeTable from 'components/tables/AttendeeTable'; -import BulkEditRegistrationDrawer from 'components/modals/BulkEditRegistrationDrawer'; - -const TeamsPage = ({ event, teams, registrationsLoading, teamsLoading, registrations, registrationsMap }) => { - const [onlyLocked, setOnlyLocked] = useState(false); - const [onlyReviewed, setOnlyReviewed] = useState(false); - const [minRating, setMinRating] = useState(0); - const [code, setCode] = useState(''); - const renderAttendees = team => { - return ( - - { - if (!team.members || !team.members.length) return null; - return ( - - {/* r._id)} - buttonProps={{ text: 'Edit all team members' }} - /> */} - - ); - }} - /> - - ); - }; - - const teamsPopulated = useMemo(() => { - return teams.map(team => { - const membersMapped = team.members - .map(member => { - return registrationsMap[member]; - }) - .filter(member => typeof member !== 'undefined'); - const ownerMapped = registrationsMap[team.owner] || {}; - const allMembers = membersMapped.concat(ownerMapped); - const reviewedCount = allMembers.filter(member => member && member.rating).length; - const memberCount = allMembers.length; - return { - ...team, - owner: ownerMapped, - members: allMembers, - avgRating: (sumBy(allMembers, m => m.rating || 0) / allMembers.length).toFixed(2), - reviewedPercent: Math.floor((reviewedCount * 100) / memberCount) - }; - }); - }, [teams, registrationsMap]); - - const teamsFiltered = teamsPopulated.filter(team => { - if (onlyLocked && !team.locked) { - return false; - } - if (onlyReviewed && team.reviewedPercent < 100) { - return false; - } - if (minRating && team.avgRating < minRating) { - return false; - } - if (code && team.code !== code) { - return false; - } - return true; - }); - - const filteredMemberIds = teamsFiltered.reduce((res, team) => { - return res.concat(team.members.map(m => m._id)); - }, []); +import TeamsTable from 'components/tables/TeamsTable'; +const TeamsPage = ({ loading, teams }) => { return ( - - - - Only locked teams - - - - Only fully reviewed teams - - - - Min. team rating - setMinRating(e.target.value)} - /> - - - Code - setCode(e.target.value)} /> - - - - {teamsFiltered.length} teams - {/* */} - - - - { - if (!registration) return '???'; - return `${registration.answers.firstName} ${registration.answers.lastName}`; - }} - /> - - members.length} - /> - a.avgRating > b.avgRating} - /> - { - if (percent === 100) { - return 100%; - } else { - return {percent}%; - } - }} - /> - (locked ? Yes : No)} - /> - + + ); }; const mapState = state => ({ teams: OrganiserSelectors.teams(state), - registrations: OrganiserSelectors.registrations(state), - event: OrganiserSelectors.event(state), - registrationsLoading: OrganiserSelectors.registrationsLoading(state), - registrationsMap: OrganiserSelectors.registrationsMap(state), - teamsLoading: OrganiserSelectors.teamsLoading(state) + loading: OrganiserSelectors.registrationsLoading(state) || OrganiserSelectors.teamsLoading(state) }); export default connect(mapState)(TeamsPage); diff --git a/shared/constants/filter-types.js b/shared/constants/filter-types.js index 30f04dbc3..ebf79b573 100644 --- a/shared/constants/filter-types.js +++ b/shared/constants/filter-types.js @@ -109,6 +109,7 @@ const numberFilterTypes = [ filterTypes.NOT_MORE_THAN.id ]; +const objectFilterTypes = [filterTypes.IS_EMPTY.id, filterTypes.NOT_EMPTY.id]; const booleanFilterTypes = [filterTypes.BOOLEAN_TRUE.id, filterTypes.BOOLEAN_FALSE.id]; const STRING = 'STRING'; @@ -116,6 +117,7 @@ const ARRAY = 'ARRAY'; const NUMBER = 'NUMBER'; const BOOLEAN = 'BOOLEAN'; const DATE = 'DATE'; +const OBJECT = 'OBJECT'; module.exports = { filterTypes, @@ -124,11 +126,13 @@ module.exports = { ARRAY: arrayFilterTypes, NUMBER: numberFilterTypes, BOOLEAN: booleanFilterTypes, + OBJECT: objectFilterTypes, DATE: [] }, STRING, ARRAY, NUMBER, BOOLEAN, - DATE + DATE, + OBJECT }; diff --git a/shared/constants/registration-fields.js b/shared/constants/registration-fields.js index 7df80a2c8..209f02db6 100644 --- a/shared/constants/registration-fields.js +++ b/shared/constants/registration-fields.js @@ -1112,7 +1112,46 @@ function buildFiltersArray() { return res.concat(filters); }, []); - return baseFilters.concat(answerFilters); + const extraFilters = [ + { + label: 'Terminal', + path: 'answers.terminal', + type: FilterTypes.OBJECT, + valueType: FilterValues.BOOLEAN + }, + { + label: 'Terminal > Motivation', + path: 'answers.terminal.motivation', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + }, + { + label: 'Terminal > Most fascinating project', + path: 'answers.terminal.mostFascinatingProject', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + }, + { + label: 'Terminal > Ideal work environment', + path: 'answers.terminal.idealWorkEnvironment', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + }, + { + label: 'Terminal > What makes you awesome', + path: 'answers.terminal.whatMakesYouAwesome', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + }, + { + label: 'Terminal > Accomodation', + path: 'answers.terminal.accomodation', + type: FilterTypes.STRING, + valueType: FilterValues.STRING + } + ]; + + return baseFilters.concat(answerFilters).concat(extraFilters); } const Helpers = {