diff --git a/backend/src/app.ts b/backend/src/app.ts index 15d7e7cf..3bfca9c2 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -13,7 +13,9 @@ import { ApartmentWithLabel, ApartmentWithId, CantFindApartmentForm, + CantFindApartmentFormWithId, QuestionForm, + QuestionFormWithId, LocationTravelTimes, } from '@common/types/db-types'; // Import Firebase configuration and types @@ -476,7 +478,7 @@ app.get('/api/pending-buildings/:status', async (req, res) => { const apartments: CantFindApartmentForm[] = apartmentDocs.map((doc) => { const data = doc.data(); const apartment = { ...data, date: data.date.toDate() } as CantFindApartmentForm; - return { ...apartment, id: doc.id } as CantFindApartmentForm; + return { ...apartment, id: doc.id } as CantFindApartmentFormWithId; }); res.status(200).send(JSON.stringify(apartments)); }); @@ -487,7 +489,7 @@ app.get('/api/contact-questions/:status', async (req, res) => { const questions: QuestionForm[] = questionDocs.map((doc) => { const data = doc.data(); const question = { ...data, date: data.date.toDate() } as QuestionForm; - return { ...question, id: doc.id } as QuestionForm; + return { ...question, id: doc.id } as QuestionFormWithId; }); res.status(200).send(JSON.stringify(questions)); }); @@ -1015,6 +1017,37 @@ app.post('/api/add-pending-building', authenticate, async (req, res) => { } }); +// API endpoint to update the status of a pending building report. +app.put( + '/api/update-pending-building-status/:buildingId/:newStatus', + authenticate, + async (req, res) => { + try { + const { buildingId, newStatus } = req.params; + + const validStatuses = ['PENDING', 'COMPLETED', 'DELETED']; + if (!validStatuses.includes(newStatus)) { + res.status(400).send('Error: Invalid status type'); + return; + } + + const doc = pendingBuildingsCollection.doc(buildingId); + + const docSnapshot = await doc.get(); + if (!docSnapshot.exists) { + res.status(404).send('Error: Building not found'); + return; + } + + await doc.update({ status: newStatus }); + res.status(200).send('Success'); + } catch (err) { + console.error(err); + res.status(500).send('Error updating building status'); + } + } +); + // API endpoint to submit a "Ask Us A Question" form. app.post('/api/add-contact-question', authenticate, async (req, res) => { try { @@ -1031,6 +1064,36 @@ app.post('/api/add-contact-question', authenticate, async (req, res) => { } }); +// API endpoint to update the status of a contact question. +app.put( + '/api/update-contact-question-status/:questionId/:newStatus', + authenticate, + async (req, res) => { + try { + const { questionId, newStatus } = req.params; + + const validStatuses = ['PENDING', 'COMPLETED', 'DELETED']; + if (!validStatuses.includes(newStatus)) { + res.status(400).send('Error: Invalid status type'); + return; + } + + const doc = contactQuestionsCollection.doc(questionId); + const docSnapshot = await doc.get(); + if (!docSnapshot.exists) { + res.status(404).send('Error: Question not found'); + return; + } + + await doc.update({ status: newStatus }); + res.status(200).send('Success'); + } catch (err) { + console.error(err); + res.status(500).send('Error updating question status'); + } + } +); + const { REACT_APP_MAPS_API_KEY } = process.env; const LANDMARKS = { eng_quad: '42.4445,-76.4836', // Duffield Hall diff --git a/common/types/db-types.ts b/common/types/db-types.ts index 5cbc4860..e8ed2c15 100644 --- a/common/types/db-types.ts +++ b/common/types/db-types.ts @@ -89,6 +89,8 @@ export type CantFindApartmentForm = { readonly userId?: string | null; }; +export type CantFindApartmentFormWithId = CantFindApartmentForm & Id; + export type QuestionForm = { readonly date: Date; readonly name: string; @@ -96,3 +98,5 @@ export type QuestionForm = { readonly msg: string; readonly userId?: string | null; }; + +export type QuestionFormWithId = QuestionForm & Id; diff --git a/frontend/src/components/Admin/AdminCantFindApt.tsx b/frontend/src/components/Admin/AdminCantFindApt.tsx index d42386c4..5ebf7534 100644 --- a/frontend/src/components/Admin/AdminCantFindApt.tsx +++ b/frontend/src/components/Admin/AdminCantFindApt.tsx @@ -1,16 +1,23 @@ import React, { ReactElement } from 'react'; -import { Card, CardContent, CardMedia, Grid, Typography } from '@material-ui/core'; +import { + Box, + Button, + Card, + CardActions, + CardContent, + CardMedia, + Grid, + Typography, +} from '@material-ui/core'; import { makeStyles } from '@material-ui/styles'; import { createAuthHeaders, getUser } from '../../utils/firebase'; +import { colors } from '../../colors'; +import axios from 'axios'; const useStyles = makeStyles(() => ({ root: { borderRadius: '10px', }, - cardContent: { - border: '1px solid #e0e0e0', - borderRadius: '10px', - }, image: { maxWidth: '30%', height: 'auto', @@ -44,6 +51,9 @@ const useStyles = makeStyles(() => ({ * Component Props for AdminCantFindApt. */ type Props = { + /** The ID of the pending building report. */ + readonly pending_building_id: string; + /** The date of the report. */ readonly date: Date; @@ -58,6 +68,9 @@ type Props = { /** Function to trigger the photo carousel. */ readonly triggerPhotoCarousel: (photos: readonly string[], startIndex: number) => void; + + /** Function to toggle the display. */ + readonly setToggle: React.Dispatch>; }; /** @@ -77,13 +90,15 @@ type Props = { * @returns {ReactElement} A Material-UI Card component displaying the apartment submission details */ const AdminCantFindApt = ({ + pending_building_id, date, apartmentName, apartmentAddress, photos = [], triggerPhotoCarousel, + setToggle, }: Props): ReactElement => { - const { root, cardContent, image, photoStyle, photoRowStyle } = useStyles(); + const { root, image, photoStyle, photoRowStyle } = useStyles(); const formattedDate = new Date(date).toLocaleString('en-US', { year: 'numeric', month: 'long', @@ -91,32 +106,79 @@ const AdminCantFindApt = ({ hour: 'numeric', minute: 'numeric', }); + + /** + * Change the status of the pending building report and trigger a re-render. + * + * @param pending_building_id - The ID of the pending building report. + * @param newStatus - The new status for the pending building report. + * @returns A promise representing the completion of the operation. + */ + const changeStatus = async (pending_building_id: string, newStatus: string) => { + const endpoint = `/api/update-pending-building-status/${pending_building_id}/${newStatus}`; + let user = await getUser(true); + if (user) { + const token = await user.getIdToken(true); + await axios.put(endpoint, {}, createAuthHeaders(token)); + setToggle((cur) => !cur); + } + }; + return ( - - - Apartment Name: {apartmentName} - Address: {apartmentAddress} - Filed Date: {formattedDate} - {photos.length > 0 && ( - - - {photos.map((photo, i) => { - return ( - triggerPhotoCarousel(photos, i)} - loading="lazy" - /> - ); - })} + + + + Apartment Name: {apartmentName} + Address: {apartmentAddress} + Filed Date: {formattedDate} + {photos.length > 0 && ( + + + {photos.map((photo, i) => { + return ( + triggerPhotoCarousel(photos, i)} + loading="lazy" + /> + ); + })} + + + )} + + + + + + { + + + + } + { + + - - )} - + } + + ); }; diff --git a/frontend/src/components/Admin/AdminContactQuestion.tsx b/frontend/src/components/Admin/AdminContactQuestion.tsx index 5dfcf23e..a0c35952 100644 --- a/frontend/src/components/Admin/AdminContactQuestion.tsx +++ b/frontend/src/components/Admin/AdminContactQuestion.tsx @@ -1,7 +1,9 @@ -import React, { ReactElement, useEffect, useState } from 'react'; -import { Card, CardContent, Typography } from '@material-ui/core'; +import React, { ReactElement } from 'react'; +import { Box, Button, Card, CardActions, CardContent, Grid, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/styles'; import { createAuthHeaders, getUser } from '../../utils/firebase'; +import { colors } from '../../colors'; +import axios from 'axios'; const useStyles = makeStyles(() => ({ root: { @@ -18,11 +20,15 @@ const useStyles = makeStyles(() => ({ })); /** - * Component Props for AdminCantFindApt. + * Component Props for AdminContactQuestion. */ type Props = { + /** The ID of the contact question. */ + readonly question_id: string; + /** The date of the question. */ readonly date: Date; + /** The name of the user. */ readonly name: string; @@ -31,6 +37,9 @@ type Props = { /** The message of the user. */ readonly msg: string; + + /** Function to toggle the display. */ + readonly setToggle: React.Dispatch>; }; /** @@ -41,14 +50,23 @@ type Props = { * the date, name, email and message from the user. Used in the admin dashboard to review * user questions and contact requests. * + * @param {string} props.question_id - The ID of the contact question. * @param {Date} props.date - The submission date/time of the contact form * @param {string} props.name - The name of the user who submitted the form * @param {string} props.email - The email address provided by the user * @param {string} props.msg - The message/question text submitted by the user + * @param {Function} props.setToggle - Function to toggle the display. * * @returns {ReactElement} - A Material-UI Card component displaying the contact form details */ -const AdminContactQuestion = ({ date, name, email, msg }: Props): ReactElement => { +const AdminContactQuestion = ({ + question_id, + date, + name, + email, + msg, + setToggle, +}: Props): ReactElement => { const classes = useStyles(); const formattedDate = new Date(date).toLocaleString('en-US', { year: 'numeric', @@ -57,14 +75,57 @@ const AdminContactQuestion = ({ date, name, email, msg }: Props): ReactElement = hour: 'numeric', minute: 'numeric', }); + + /** + * Change the status of the contact question and trigger a re-render. + * + * @param question_id - The ID of the contact question. + * @param newStatus - The new status for the contact question. + * @returns A promise representing the completion of the operation. + */ + const changeStatus = async (question_id: string, newStatus: string) => { + const endpoint = `/api/update-contact-question-status/${question_id}/${newStatus}`; + let user = await getUser(true); + if (user) { + const token = await user.getIdToken(true); + await axios.put(endpoint, {}, createAuthHeaders(token)); + setToggle((cur) => !cur); + } + }; + return ( - - - Date: {formattedDate} - Name: {name} - Email: {email} - Msg: {msg} - + + + + Date: {formattedDate} + Name: {name} + Email: {email} + Message: {msg} + + + + + + + + + + + + + ); }; diff --git a/frontend/src/pages/AdminPage.tsx b/frontend/src/pages/AdminPage.tsx index ad7f5266..0584489d 100644 --- a/frontend/src/pages/AdminPage.tsx +++ b/frontend/src/pages/AdminPage.tsx @@ -13,7 +13,11 @@ import { Tab, IconButton, } from '@material-ui/core'; -import { CantFindApartmentForm, QuestionForm, ReviewWithId } from '../../../common/types/db-types'; +import { + CantFindApartmentFormWithId, + QuestionFormWithId, + ReviewWithId, +} from '../../../common/types/db-types'; import { get } from '../utils/call'; import AdminReviewComponent from '../components/Admin/AdminReview'; import { useTitle } from '../utils'; @@ -71,8 +75,8 @@ const AdminPage = (): ReactElement => { const [dtownReviewCount, setDtownReviewCount] = useState({ count: 0 }); const [northReviewCount, setNorthReviewCount] = useState({ count: 0 }); const [toggle, setToggle] = useState(false); - const [pendingApartment, setPendingApartmentData] = useState([]); - const [pendingContactQuestions, setPendingContactQuestions] = useState([]); + const [pendingApartment, setPendingApartmentData] = useState([]); + const [pendingContactQuestions, setPendingContactQuestions] = useState([]); const [pendingExpanded, setPendingExpanded] = useState(true); const [declinedExpanded, setDeclinedExpanded] = useState(true); const [reportedData, setReportedData] = useState([]); @@ -129,21 +133,22 @@ const AdminPage = (): ReactElement => { useEffect(() => { const apartmentTypes = new Map< string, - React.Dispatch> + React.Dispatch> >([['PENDING', setPendingApartmentData]]); apartmentTypes.forEach((cllbck, apartmentType) => { - get(`/api/pending-buildings/${apartmentType}`, { + get(`/api/pending-buildings/${apartmentType}`, { callback: cllbck, }); }); }, [toggle]); useEffect(() => { - const questionTypes = new Map>>([ - ['PENDING', setPendingContactQuestions], - ]); + const questionTypes = new Map< + string, + React.Dispatch> + >([['PENDING', setPendingContactQuestions]]); questionTypes.forEach((cllbck, questionType) => { - get(`/api/contact-questions/${questionType}`, { + get(`/api/contact-questions/${questionType}`, { callback: cllbck, }); }); @@ -306,11 +311,13 @@ const AdminPage = (): ReactElement => { .map((apartment, index) => ( ))} @@ -325,10 +332,12 @@ const AdminPage = (): ReactElement => { .map((question, index) => ( ))}