From 2117f9b455505c4481477655b45d0404cd4d26ff Mon Sep 17 00:00:00 2001 From: hunterMotko <87234197+hunterMotko@users.noreply.github.com> Date: Fri, 24 Dec 2021 13:05:45 -0500 Subject: [PATCH] feat: admin messaging (#236) * feat: unresolved-messaging * feat: unresolved-messaging * feat(messaging): unresolved survey and announce * feat(messaging): unresolved survey and announce * feat(messaging): unresolved survey and announce * fix: message to grower context issue * fix: messaging context * fix: pr review fixes Co-authored-by: Nick Charlton --- .env.development | 1 + .prettierrc | 2 +- src/api/messaging.js | 77 +++++ src/components/GrowerDetail.js | 36 ++- src/components/Messaging/AnnounceMessage.js | 216 ++++++++++++++ src/components/Messaging/Inbox.js | 120 ++++++++ src/components/Messaging/MessageBody.js | 297 +++++++++++++++++++ src/components/Messaging/Messaging.js | 155 ++++++++++ src/components/Messaging/SearchInbox.js | 35 +++ src/components/Messaging/Survey.js | 307 ++++++++++++++++++++ src/components/Messaging/TextInput.js | 54 ++++ src/context/AppContext.js | 19 +- src/context/MessagingContext.js | 144 +++++++++ src/models/auth.js | 1 + src/views/MessagingView.js | 14 + 15 files changed, 1471 insertions(+), 7 deletions(-) create mode 100644 src/api/messaging.js create mode 100644 src/components/Messaging/AnnounceMessage.js create mode 100644 src/components/Messaging/Inbox.js create mode 100644 src/components/Messaging/MessageBody.js create mode 100644 src/components/Messaging/Messaging.js create mode 100644 src/components/Messaging/SearchInbox.js create mode 100644 src/components/Messaging/Survey.js create mode 100644 src/components/Messaging/TextInput.js create mode 100644 src/context/MessagingContext.js create mode 100644 src/views/MessagingView.js diff --git a/.env.development b/.env.development index c52094ad4..c64b4a35c 100644 --- a/.env.development +++ b/.env.development @@ -1,5 +1,6 @@ REACT_APP_WEBMAP_DOMAIN=http://dev.treetracker.org REACT_APP_API_ROOT=https://dev-k8s.treetracker.org/api/admin +REACT_APP_MESSAGING_ROOT=https://dev-k8s.treetracker.org/messaging REACT_APP_TREETRACKER_API_ROOT=https://dev-k8s.treetracker.org/treetracker REACT_APP_ENABLE_CAPTURE_MATCHING=true REACT_APP_REPORTING_API_ROOT=https://dev-k8s.treetracker.org/reporting diff --git a/.prettierrc b/.prettierrc index 261acc0bf..025e07033 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,6 +4,6 @@ "singleQuote": true, "semi": true, "tabWidth": 2, - "trailingComma": "all", + "trailingComma": "es5", "bracketSpacing": true } diff --git a/src/api/messaging.js b/src/api/messaging.js new file mode 100644 index 000000000..54a05d02c --- /dev/null +++ b/src/api/messaging.js @@ -0,0 +1,77 @@ +import { handleResponse, handleError } from './apiUtils'; +import { session } from '../models/auth'; + +export default { + getRegion() { + const query = `${process.env.REACT_APP_MESSAGING_ROOT}/region`; + + return fetch(query, { + method: 'GET', + headers: { + 'content-type': 'application/json', + Authorization: session.token, + }, + }) + .then(handleResponse) + .catch(handleError); + }, + postRegion(payload) { + const query = `${process.env.REACT_APP_MESSAGING_ROOT}/region`; + const { id, name, description, created_at } = payload; + + return fetch(query, { + method: 'POST', + headers: { + 'content-type': 'application/json', + Authorization: session.token, + }, + body: JSON.stringify({ id, name, description, created_at }), + }) + .then(handleResponse) + .catch(handleError); + }, + getRegionById(region_id) { + const query = `${process.env.REACT_APP_MESSAGING_ROOT}/region/${region_id}`; + + return fetch(query, { + headers: { + Authorization: session.token, + }, + }) + .then(handleResponse) + .catch(handleError); + }, + getMessage(author_handle) { + const query = `${process.env.REACT_APP_MESSAGING_ROOT}/message?author_handle=${author_handle}`; + + return fetch(query).then(handleResponse).catch(handleError); + }, + postMessage(payload) { + const query = `${process.env.REACT_APP_MESSAGING_ROOT}/message`; + + return fetch(query, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: session.token, + }, + body: JSON.stringify({ ...payload }), + }) + .then(handleResponse) + .catch(handleError); + }, + postMessageSend(payload) { + const query = `${process.env.REACT_APP_MESSAGING_ROOT}/message/send`; + + return fetch(query, { + method: 'POST', + headers: { + 'content-type': 'application/json', + Authorization: session.token, + }, + body: JSON.stringify(payload), + }) + .then(handleResponse) + .catch(handleError); + }, +}; diff --git a/src/components/GrowerDetail.js b/src/components/GrowerDetail.js index e6eb2ca4f..b3a682931 100644 --- a/src/components/GrowerDetail.js +++ b/src/components/GrowerDetail.js @@ -1,4 +1,5 @@ import React, { useState, useEffect, useContext } from 'react'; +import { Link } from 'react-router-dom'; import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; import CardMedia from '@material-ui/core/CardMedia'; @@ -17,12 +18,14 @@ import Divider from '@material-ui/core/Divider'; import EditIcon from '@material-ui/icons/Edit'; import { LinearProgress } from '@material-ui/core'; import { Done, Clear, HourglassEmptyOutlined } from '@material-ui/icons'; +import Button from '@material-ui/core/Button'; import Fab from '@material-ui/core/Fab'; import api from '../api/growers'; import { getDateTimeStringLocale } from '../common/locale'; import { hasPermission, POLICIES } from '../models/auth'; import { AppContext } from '../context/AppContext'; import { GrowerContext } from '../context/GrowerContext'; +import { MessagingContext } from 'context/MessagingContext'; import EditGrower from './EditGrower'; import OptimizedImage from './OptimizedImage'; import LinkToWebmap from './common/LinkToWebmap'; @@ -87,6 +90,19 @@ const useStyle = makeStyles((theme) => ({ fontWeight: 700, fontSize: '0.8em', }, + messageButton: { + background: theme.palette.primary.main, + color: 'white', + position: 'relative', + right: -175, + bottom: 90, + borderRadius: '25px', + '&:hover': { + backgroundColor: '#fff', + borderColor: theme.palette.primary.main, + color: theme.palette.primary.main, + }, + }, })); const GrowerDetail = (props) => { @@ -95,6 +111,7 @@ const GrowerDetail = (props) => { const { growerId } = props; const appContext = useContext(AppContext); const growerContext = useContext(GrowerContext); + const { sendMessageFromGrower } = useContext(MessagingContext); const [growerRegistrations, setGrowerRegistrations] = useState(null); const [editDialogOpen, setEditDialogOpen] = useState(false); const [grower, setGrower] = useState({}); @@ -125,7 +142,7 @@ const GrowerDetail = (props) => { console.log('grower registrations: ', registrations); if (registrations && registrations.length) { const sortedRegistrations = registrations.sort((a, b) => - a.created_at > b.created_at ? 1 : -1, + a.created_at > b.created_at ? 1 : -1 ); setGrowerRegistrations(sortedRegistrations); setDeviceIdentifiers( @@ -137,7 +154,7 @@ const GrowerDetail = (props) => { ? 'iOS' : 'Android', })) - .filter((id) => id), + .filter((id) => id) ); } }); @@ -270,6 +287,18 @@ const GrowerDetail = (props) => { ID: + {hasPermission(appContext.user, [POLICIES.SUPER_PERMISSION]) && ( + + + + )} Captures @@ -385,8 +414,7 @@ const GrowerDetail = (props) => { growerRegistrations .map((item) => item.country) .filter( - (country, i, arr) => - country && arr.indexOf(country) === i, + (country, i, arr) => country && arr.indexOf(country) === i ) .join(', ')) || '---'} diff --git a/src/components/Messaging/AnnounceMessage.js b/src/components/Messaging/AnnounceMessage.js new file mode 100644 index 000000000..cdef921c3 --- /dev/null +++ b/src/components/Messaging/AnnounceMessage.js @@ -0,0 +1,216 @@ +import React, { useState, useContext } from 'react'; +import { + SwipeableDrawer, + Grid, + Typography, + IconButton, + TextField, + Button, + Select, + MenuItem, + OutlinedInput, +} from '@material-ui/core'; +import { Close } from '@material-ui/icons'; +import { makeStyles } from '@material-ui/styles'; +import GSInputLabel from 'components/common/InputLabel'; +import { AppContext } from 'context/AppContext'; +import { MessagingContext } from 'context/MessagingContext'; +const DRAWER_WIDTH = 300; + +const useStyles = makeStyles((theme) => ({ + drawer: { + background: 'light tan', + width: DRAWER_WIDTH, + padding: theme.spacing(3), + }, + title: { + width: '100%', + display: 'flex', + flexFlow: 'row', + }, + formTitle: { + color: theme.palette.primary.main, + }, + directions: { + color: 'grey', + margin: '5px', + wordWrap: 'break-word', + }, + closeAnnounce: { + marginLeft: 'auto', + border: '1px solid ' + `${theme.palette.primary.main}`, + }, + form: { + width: DRAWER_WIDTH - 20, + display: 'flex', + flexDirection: 'column', + justifycontent: 'center', + marginBottom: '2em', + }, + sendButton: { + background: theme.palette.primary.main, + color: 'white', + marginTop: '10px', + }, +})); + +const MenuProps = { + PaperProps: { + style: { + maxHeight: 48 * 4.5 + 8, + width: 250, + }, + }, +}; + +const AnnounceMessageForm = () => { + const { orgList } = useContext(AppContext); + const { user, regions, postMessageSend } = useContext(MessagingContext); + const { form, sendButton } = useStyles(); + const [values, setValues] = useState({ + message: '', + videoLink: '', + organization: '', + region: '', + }); + + const handleChange = (e) => { + const { name, value } = e.target; + setValues({ + ...values, + [name]: value, + }); + }; + const { message, videoLink, organization, region } = values; + + const handleSubmit = async (e) => { + e.preventDefault(); + + const payload = { + author_handle: user.userName, + subject: 'Announce Message', + body: values.message, + }; + if (region) { + payload['region_id'] = region; + } + if (organization) { + payload['organization_id'] = organization; + } + if (payload.organization_id || payload.region_id) { + await postMessageSend(payload); + } + }; + + return ( +
+ + + + + + + + + + + ); +}; + +const AnnounceMessage = ({ + toggleAnnounceMessage, + setToggleAnnounceMessage, +}) => { + const iOS = + typeof navigator !== 'undefined' && + typeof navigator.userAgent !== 'undefined' && + /iPad|iPhone|iPod/.test(navigator.userAgent); + + const { drawer, title, formTitle, directions, closeAnnounce } = useStyles(); + + return ( + setToggleAnnounceMessage(true)} + onClose={() => setToggleAnnounceMessage(false)} + classes={{ paper: drawer }} + PaperProps={{ elevation: 6 }} + > + + + + Announce Message + + setToggleAnnounceMessage(false)} + > + + + + + + Write a group message or notification below. Then select the target + audience for your message. All replies will be available in you + Messaging Inbox. + + + + + + + + ); +}; + +export default AnnounceMessage; diff --git a/src/components/Messaging/Inbox.js b/src/components/Messaging/Inbox.js new file mode 100644 index 000000000..fb5447b5b --- /dev/null +++ b/src/components/Messaging/Inbox.js @@ -0,0 +1,120 @@ +import React, { useState } from 'react'; +import { + List, + ListItem, + ListItemText, + ListItemAvatar, + Avatar, + Typography, + Paper, + Button, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/styles'; +import SearchInbox from './SearchInbox'; + +const useStyles = makeStyles((theme) => ({ + paper: { + height: '100%', + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + }, + searchInbox: { + alignSelf: 'flex-end', + }, + list: { + height: '100%', + width: '100%', + overflow: 'auto', + padding: 0, + }, + listItem: { + border: '1px solid lightGrey', + height: '5em', + '&.Mui-selected': { + background: '#FFF', + borderRight: `5px solid ${theme.palette.primary.main}`, + borderLeft: `5px solid ${theme.palette.primary.main}`, + }, + }, + listText: { + [theme.breakpoints.down('sm')]: { + justifyContent: 'center', + }, + }, + primaryText: { + fontSize: '16', + fontWeight: 'bold', + }, + messageText: { + [theme.breakpoints.down('sm')]: { + display: 'none', + }, + }, + avatar: { + [theme.breakpoints.down('xs')]: { + display: 'none', + }, + }, +})); + +const Inbox = ({ messages, selectedIndex, handleListItemClick }) => { + const { + paper, + searchInbox, + list, + listItem, + listText, + primaryText, + avatar, + } = useStyles(); + + const [search, setSearch] = useState(''); + + return ( + + + {messages + .filter((message) => { + if (search === '') { + return message; + } else if ( + message.userName.toLowerCase().includes(search.toLowerCase()) + ) { + return message; + } + }) + .map((message, i) => ( + handleListItemClick(e, i, message.userName)} + > + { + <> + + + + + {' '} + {message.userName} + + } + className={listText} + /> + + } + + ))} + + + + ); +}; + +export default Inbox; diff --git a/src/components/Messaging/MessageBody.js b/src/components/Messaging/MessageBody.js new file mode 100644 index 000000000..cfd707c9c --- /dev/null +++ b/src/components/Messaging/MessageBody.js @@ -0,0 +1,297 @@ +import React, { useState, useContext } from 'react'; +import { createStyles, makeStyles } from '@material-ui/core/styles'; +import { Avatar, Grid, Typography, Paper } from '@material-ui/core'; +import { TextInput } from './TextInput.js'; + +import { MessagingContext } from 'context/MessagingContext.js'; + +const useStyles = makeStyles((theme) => + createStyles({ + messageRow: { + display: 'flex', + }, + messageRowRight: { + display: 'flex', + justifyContent: 'flex-end', + }, + recievedMessage: { + position: 'relative', + marginLeft: '20px', + marginBottom: '10px', + padding: '10px', + backgroundColor: 'lightGrey', + textAlign: 'left', + borderRadius: '10px', + }, + sentMessage: { + position: 'relative', + marginRight: '20px', + marginBottom: '10px', + padding: '10px', + backgroundColor: theme.palette.primary.main, + textAlign: 'left', + borderRadius: '10px', + }, + messageContent: { + padding: 0, + margin: 0, + wordWrap: 'break-word', + }, + messageTimeStampLeft: { + display: 'flex', + marginRight: 'auto', + alignItems: 'center', + color: 'grey', + }, + messageTimeStampRight: { + display: 'flex', + marginLeft: 'auto', + alignItems: 'center', + color: 'grey', + }, + displayName: { + marginLeft: '20px', + }, + paper: { + height: '100%', + display: 'flex', + alignItems: 'center', + flexDirection: 'column', + }, + senderInfo: { + padding: '10px', + borderBottom: '2px solid black', + }, + messagesBody: { + height: '80%', + width: '95%', + padding: '7.5px', + overflowY: 'auto', + }, + senderItem: { + padding: theme.spacing(1), + margin: theme.spacing(1), + }, + avatar: { + width: '5em', + height: '5em', + [theme.breakpoints.down('md')]: { + width: '4em', + height: '4em', + }, + [theme.breakpoints.down('sm')]: { + width: '2em', + height: '2em', + }, + }, + textInput: { + borderTop: '2px solid black', + }, + surveyContent: { + color: '#fff', + }, + }) +); + +export const AnnounceMessage = ({ message }) => { + const { + messageRow, + recievedMessage, + messageContent, + messageTimeStampRight, + } = useStyles(); + + return ( +
+
+
+ {message.body} +
+
+ + {message.composed_at.slice(0, 10)} + +
+ ); +}; + +export const SurveyMessage = ({ message }) => { + const { messageRowRight, sentMessage, surveyContent } = useStyles(); + + const { questions } = message.survey; + return ( +
+ + + {message.body ? message.body : ''} + + {questions.map((question, i) => ( +
+ + Question {i + 1}:{' '} + {question.prompt} + + + Choices: +
    + {question.choices.map((choice) => ( +
  1. {choice}
  2. + ))} +
+
+
+ ))} +
+
+ ); +}; + +export const RecievedMessage = ({ message }) => { + const { + messageRow, + messageTimeStampRight, + messageContent, + recievedMessage, + } = useStyles(); + return ( + <> +
+
+
+ {message.body} +
+
+ + {message.composed_at.slice(0, 10)} + +
+ + ); +}; + +export const SentMessage = ({ message }) => { + const { + messageRowRight, + messageContent, + messageTimeStampLeft, + sentMessage, + } = useStyles(); + + return ( + <> + {message.body ? ( + + + {message.composed_at.slice(0, 10)} + + + {message.body} + + + ) : ( +
+ )} + + ); +}; + +const SenderInformation = ({ messageRecipient, id }) => { + const { senderInfo, senderItem, avatar } = useStyles(); + + return ( + + + + + + {messageRecipient} + + ID:{id} + + + + ); +}; + +const MessageBody = ({ messages, messageRecipient }) => { + const { paper, messagesBody, textInput } = useStyles(); + const { user, postMessageSend } = useContext(MessagingContext); + const [messageContent, setMessageContent] = useState(''); + + const handleSubmit = async (e) => { + e.preventDefault(); + + let lastMessage = messages[messages.length - 1]; + + const messagePayload = { + parent_message_id: lastMessage.id ? lastMessage.id : null, + author_handle: user.userName, + recipient_handle: messageRecipient, + subject: 'Message', + body: messageContent, + }; + + if (messageContent !== '') { + if (user.userName && messageRecipient) { + await postMessageSend(messagePayload); + } + } + setMessageContent(''); + }; + + return ( + + {messageRecipient && messages ? ( + + ) : ( + + )} +
+ {messages ? ( + messages.map((message, i) => { + if (message.subject === 'Message') { + return message.from === user.userName ? ( + + ) : message.body.length > 1 ? ( + + ) : ( +
+ ); + } else if (message.subject.includes('Survey')) { + return ( + + ); + } else if (message.subject.includes('Announce')) { + return ( + + ); + } + }) + ) : ( +
Loading ...
+ )} +
+ +
+ ); +}; + +export default MessageBody; diff --git a/src/components/Messaging/Messaging.js b/src/components/Messaging/Messaging.js new file mode 100644 index 000000000..5f6f35e0c --- /dev/null +++ b/src/components/Messaging/Messaging.js @@ -0,0 +1,155 @@ +import React, { useState, useEffect, useContext } from 'react'; + +import Navbar from 'components/Navbar'; +import Inbox from './Inbox'; +import MessageBody from './MessageBody'; +import Survey from './Survey'; +import AnnounceMessage from './AnnounceMessage'; + +import Grid from '@material-ui/core/Grid'; +import Button from '@material-ui/core/Button'; +import { makeStyles } from '@material-ui/styles'; +import { MessagingContext } from '../../context/MessagingContext'; + +const useStyles = makeStyles((theme) => ({ + rootContainer: { + paddingBottom: '1em', + marginBottom: '2em', + }, + title: { + marginLeft: theme.spacing(7), + fontSize: '24px', + }, + button: { + backgroundColor: theme.palette.primary.main, + borderRadius: '50px', + color: 'white', + margin: '5px', + }, + buttonContainer: { + margin: '2em', + marginLeft: 'auto', + }, + messagesContainer: { + margin: '2em', + padding: '2em', + }, + container: { + width: '90vw', + height: '85vh', + marginLeft: 'auto', + marginRight: 'auto', + marginBottom: '6em', + border: '1px solid black', + borderRadius: '5px', + }, + inbox: { + width: '30%', + height: '100%', + border: '1px solid black', + }, + body: { + height: '100%', + width: '100%', + border: '1px solid black', + }, +})); + +const Messaging = () => { + // styles + const { + rootContainer, + title, + button, + buttonContainer, + container, + inbox, + body, + } = useStyles(); + + const { messages, loadMessages, loadRegions } = useContext(MessagingContext); + + useEffect(() => { + loadMessages(); + loadRegions(); + }, []); + + const [toggleAnnounceMessage, setToggleAnnounceMessage] = useState(false); + const [toggleSurvey, setToggleSurvey] = useState(false); + const [messageRecipient, setMessageRecipient] = useState(''); + const [selectedIndex, setSelectedIndex] = useState(0); + + useEffect(() => { + if (messages.length && messageRecipient === '') { + setMessageRecipient(messages[0].userName); + } + }, [messages]); + + const handleListItemClick = (e, i, userName) => { + setSelectedIndex(i); + setMessageRecipient(userName); + }; + + return ( + <> + + + +

Inbox

+
+ + + + {toggleAnnounceMessage ? ( + + ) : ( + <> + )} + {toggleSurvey ? ( + + ) : ( + <> + )} + +
+ + + + + + {messages.length ? ( + + ) : ( + + )} + + + + ); +}; + +export default Messaging; diff --git a/src/components/Messaging/SearchInbox.js b/src/components/Messaging/SearchInbox.js new file mode 100644 index 000000000..6f2329b25 --- /dev/null +++ b/src/components/Messaging/SearchInbox.js @@ -0,0 +1,35 @@ +import React from 'react'; + +import { makeStyles } from '@material-ui/styles'; +import { TextField } from '@material-ui/core'; + +const useStyles = makeStyles((theme) => ({ + wrapForm: { + width: '100%', + borderTop: '2px solid black', + }, + field: { + width: '100%', + color: theme.palette.primary.main, + wordWrap: 'break-word', + }, +})); + +const SearchInbox = ({ setSearch }) => { + const { wrapForm, field } = useStyles(); + + return ( +
+ setSearch(e.target.value)} + /> + + ); +}; + +export default SearchInbox; diff --git a/src/components/Messaging/Survey.js b/src/components/Messaging/Survey.js new file mode 100644 index 000000000..397a4ad42 --- /dev/null +++ b/src/components/Messaging/Survey.js @@ -0,0 +1,307 @@ +import React, { useState, useContext } from 'react'; +import { + SwipeableDrawer, + TextField, + Grid, + Typography, + Button, + IconButton, + MenuItem, + Select, + FormControl, + OutlinedInput, +} from '@material-ui/core'; +import { Close } from '@material-ui/icons'; +import { makeStyles } from '@material-ui/styles'; + +import GSInputLabel from 'components/common/InputLabel'; +import { MessagingContext } from 'context/MessagingContext'; +import { AppContext } from 'context/AppContext'; + +const DRAWER_WIDTH = 300; + +const useStyles = makeStyles((theme) => ({ + drawer: { + background: 'light tan', + width: DRAWER_WIDTH, + padding: theme.spacing(3), + }, + title: { + width: '100%', + display: 'flex', + flexFlow: 'row', + }, + formTitle: { + color: theme.palette.primary.main, + }, + directions: { + color: 'grey', + margin: '5px', + wordWrap: 'break-word', + }, + form: { + width: DRAWER_WIDTH - 20, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + marginBottom: '2em', + }, + input: { + margin: '5px', + '& .MuiInputBase-input': { + fontSize: 16, + padding: '10px 12px', + }, + }, + submitButton: { + margin: '10px auto', + width: DRAWER_WIDTH - 50, + background: theme.palette.primary.main, + color: 'white', + }, + surveyCloseButton: { + marginLeft: 'auto', + border: '1px solid ' + `${theme.palette.primary.main}`, + }, +})); + +const SurveyForm = () => { + const { form, submitButton, input } = useStyles(); + const { user, regions, postMessageSend } = useContext(MessagingContext); + const { orgList } = useContext(AppContext); + const [questionOne, setQuestionOne] = useState({ + prompt: '', + choiceOne: '', + choiceTwo: '', + choiceThree: '', + }); + const [questionTwo, setQuestionTwo] = useState({ + prompt: '', + choiceOne: '', + choiceTwo: '', + choiceThree: '', + }); + const [questionThree, setQuestionThree] = useState({ + prompt: '', + choiceOne: '', + choiceTwo: '', + choiceThree: '', + }); + + const [values, setValues] = useState({ region: '', organization: '' }); + + const handleQuestionsChange = (e, question) => { + const { name, value } = e.target; + if (question === 'questionOne') { + setQuestionOne({ + ...questionOne, + [name]: value, + }); + } else if (question === 'questionTwo') { + setQuestionTwo({ + ...questionTwo, + [name]: value, + }); + } else if (question === 'questionThree') { + setQuestionThree({ + ...questionThree, + [name]: value, + }); + } + }; + + const handleChange = (e) => { + const { name, value } = e.target; + setValues({ + ...values, + [name]: value, + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + const allQuestions = { questionOne, questionTwo, questionThree }; + const payload = { + author_handle: user.userName, + subject: 'Survey', + body: 'Survey', + survey: { + title: questionOne.prompt, + questions: [], + }, + }; + + if (values.region.length > 1) { + payload['region_id'] = values.region; + } + + if (values.organization.length > 1) { + payload['organization_id'] = values.organization; + } + + Object.values(allQuestions).map((question) => { + const { prompt, choiceOne, choiceTwo, choiceThree } = question; + if (prompt.length > 1 && choiceOne && choiceTwo && choiceThree) { + payload.survey.questions.push({ + prompt, + choices: [choiceOne, choiceTwo, choiceThree], + }); + } + }); + + try { + if (payload.author_handle && payload.survey.title.length > 1) { + await postMessageSend(payload); + } + } catch (err) { + console.log(err); + } + }; + return ( +
+ {['One', 'Two', 'Three'].map((num) => ( +
+ + handleQuestionsChange(e, `question${num}`)} + /> + + handleQuestionsChange(e, `question${num}`)} + /> + handleQuestionsChange(e, `question${num}`)} + /> + handleQuestionsChange(e, `question${num}`)} + /> +
+ ))} +
+ + + + + + + + +
+ +
+ ); +}; + +const Survey = ({ toggleSurvey, setToggleSurvey }) => { + const iOS = + typeof navigator !== 'undefined' && + typeof navigator.userAgent !== 'undefined' && + /iPad|iPhone|iPod/.test(navigator.userAgent); + + const { + title, + formTitle, + directions, + drawer, + surveyCloseButton, + } = useStyles(); + + return ( + <> + setToggleSurvey(false)} + onOpen={() => setToggleSurvey} + classes={{ paper: drawer }} + PaperProps={{ elevation: 6 }} + > + + + + Quick Surveys + + setToggleSurvey(false)} + > + + + + + + Write a survey question and up to 3 answering options. Then select + the target audience for the survey. All replies will be available + in your Messaging Ibox. + + + + + + + + + ); +}; + +export default Survey; diff --git a/src/components/Messaging/TextInput.js b/src/components/Messaging/TextInput.js new file mode 100644 index 000000000..fcffb717f --- /dev/null +++ b/src/components/Messaging/TextInput.js @@ -0,0 +1,54 @@ +import React from 'react'; +import TextField from '@material-ui/core/TextField'; +import { makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; + +const useStyles = makeStyles((theme) => ({ + wrapForm: { + display: 'flex', + alignSelf: 'flex-end', + width: '100%', + borderTop: '2px solid black', + }, + wrapText: { + width: '100%', + }, + button: { + background: 'white', + color: theme.palette.primary.main, + fontWeight: 'bold', + }, +})); + +export const TextInput = ({ + handleSubmit, + messageContent, + setMessageContent, +}) => { + const { wrapForm, wrapText, button } = useStyles(); + + const handleChange = (e) => { + setMessageContent(e.target.value); + }; + + return ( +
+ + + + ); +}; diff --git a/src/context/AppContext.js b/src/context/AppContext.js index bd22a4539..88aafcef5 100644 --- a/src/context/AppContext.js +++ b/src/context/AppContext.js @@ -6,11 +6,13 @@ import VerifyView from '../views/VerifyView'; import GrowersView from '../views/GrowersView'; import CapturesView from '../views/CapturesView'; import EarningsView from '../views/EarningsView/EarningsView'; +import MessagingView from 'views/MessagingView'; import Account from '../components/Account'; import Home from '../components/Home/Home'; import Users from '../components/Users'; import SpeciesView from '../views/SpeciesView'; import CaptureMatchingView from '../components/CaptureMatching/CaptureMatchingView'; +import { MessagingProvider } from './MessagingContext'; import Unauthorized from '../components/Unauthorized'; import IconSettings from '@material-ui/icons/Settings'; @@ -25,6 +27,7 @@ import CategoryIcon from '@material-ui/icons/Category'; import HomeIcon from '@material-ui/icons/Home'; import CompareIcon from '@material-ui/icons/Compare'; import CreditCardIcon from '@material-ui/icons/CreditCard'; +import InboxRounded from '@material-ui/icons/InboxRounded'; import { session, hasPermission, POLICIES } from '../models/auth'; import api from '../api/treeTrackerApi'; @@ -134,6 +137,16 @@ function getRoutes(user) { icon: IconPermIdentity, disabled: false, }, + { + name: 'Inbox', + linkTo: '/messaging', + component: MessagingView, + icon: InboxRounded, + disabled: !hasPermission(user, [ + POLICIES.SUPER_PERMISSION, + POLICIES.SEND_MESSAGES, + ]), + }, ]; } @@ -169,7 +182,7 @@ export const AppProvider = (props) => { headers: { Authorization: localToken, }, - }, + } ) .then((response) => { // console.log('CONTEXT CHECK SESSION', response.data, 'USER', localUser, 'TOKEN', localToken); @@ -245,6 +258,8 @@ export const AppProvider = (props) => { // VerifyProvider and GrowerProvider need to wrap children here so that they are available when needed return ( - {props.children} + + {props.children} + ); }; diff --git a/src/context/MessagingContext.js b/src/context/MessagingContext.js new file mode 100644 index 000000000..d6b557dbf --- /dev/null +++ b/src/context/MessagingContext.js @@ -0,0 +1,144 @@ +import React, { useState, createContext } from 'react'; +import api from '../api/messaging'; + +export const MessagingContext = createContext({ + user: {}, + messages: [], + resMessages: [], + growerMessage: {}, + regions: [], + sendMessageFromGrower: () => {}, + loadMessages: () => {}, + loadRegions: () => {}, + postRegion: () => {}, + getRegionById: () => {}, + postMessage: () => {}, + postMessageSend: () => {}, +}); + +export const MessagingProvider = (props) => { + const [regions, setRegions] = useState([]); + const [messages, setMessages] = useState([]); + const [growerMessage, setGrowerMessage] = useState({}); + const user = JSON.parse(localStorage.getItem('user')); + + const groupMessageByHandle = (rawMessages) => { + // make key of recipients name and group messages together + let newMessages = rawMessages + .sort((a, b) => (a.composed_at < b.composed_at ? -1 : 1)) + .reduce((grouped, message) => { + if (message.subject === 'Message') { + let key = + message.to !== user.userName ? message[`to`] : message['from']; + if (key) { + if (!grouped[key] && !messages[key]) { + grouped[key] = []; + } + grouped[key].push(message); + } + } else if (message.subject === 'Survey') { + let key = message.survey.title; + if (grouped[key]) { + if (grouped[key].survey.id === message.survey.id) { + return; + } else { + grouped[key] = []; + } + } else { + grouped[key] = []; + } + grouped[key].push(message); + } else if (message.subject === 'Announce Message') { + let key = + message.to !== user.userName ? message[`to`] : message['from']; + if (key) { + if (!grouped[key] && !messages[key]) { + grouped[key] = []; + } + grouped[key].push(message); + } + } + return grouped; + }, {}); + setMessages([ + ...Object.entries(newMessages).map(([key, val]) => { + if (key && val) { + return { + userName: key, + messages: val, + }; + } + }), + ]); + }; + + const postRegion = async (payload) => { + await api.postRegion(payload); + }; + + const getRegionById = async (id) => { + await api.getRegionById(id); + }; + + const postMessage = async (payload) => { + await api.postMessage(payload); + }; + + const postMessageSend = async (payload) => { + if (payload) { + await api.postMessageSend(payload); + } else { + return 'Were sorry something went wrong. Please try again.'; + } + }; + + const sendMessageFromGrower = (grower) => { + const payload = { + body: '', + from: user.userName, + subject: 'Message', + to: grower.phone ? grower.phone : grower.email, + }; + + if (payload.to) { + setGrowerMessage(payload); + } + }; + + const loadMessages = async () => { + const res = await api.getMessage(user.userName); + + if (res && growerMessage) { + groupMessageByHandle([growerMessage, ...res.messages]); + } else { + groupMessageByHandle(res.messages); + } + }; + + const loadRegions = async () => { + const res = await api.getRegion(); + + if (res) { + setRegions(res); + } + }; + + const value = { + user, + messages, + regions, + sendMessageFromGrower, + loadMessages, + loadRegions, + postRegion, + getRegionById, + postMessage, + postMessageSend, + }; + + return ( + + {props.children} + + ); +}; diff --git a/src/models/auth.js b/src/models/auth.js index d511f64ae..b722f4761 100644 --- a/src/models/auth.js +++ b/src/models/auth.js @@ -12,6 +12,7 @@ const POLICIES = { APPROVE_TREE: 'approve_tree', LIST_GROWER: 'list_planter', MANAGE_GROWER: 'manage_planter', + SEND_MESSAGES: 'send_messages', }; function hasPermission(user, p) { diff --git a/src/views/MessagingView.js b/src/views/MessagingView.js new file mode 100644 index 000000000..2aaff74a8 --- /dev/null +++ b/src/views/MessagingView.js @@ -0,0 +1,14 @@ +import React, { useEffect } from 'react'; +import { documentTitle } from '../common/variables'; +import Messaging from 'components/Messaging/Messaging'; + +const MessagingView = () => { + // set Title + useEffect(() => { + document.title = `Messaging - ${documentTitle}`; + }, []); + + return ; +}; + +export default MessagingView;