diff --git a/src/api/test/protectedApiClient.test.ts b/src/api/test/protectedApiClient.test.ts index 7b846fea..25b84f6a 100644 --- a/src/api/test/protectedApiClient.test.ts +++ b/src/api/test/protectedApiClient.test.ts @@ -2308,6 +2308,69 @@ describe('Admin Protected Client Routes', () => { }); }); + describe('filterSiteImages', () => { + it('makes the right request', async () => { + const response = ''; + + const params = { + submittedStart: null, + submittedEnd: null, + neighborhoods: [0, 1, 2], + siteIds: [0, 1, 2, 3], + }; + + nock(BASE_URL) + .get(ParameterizedAdminApiRoutes.FILTER_SITE_IMAGES(params)) + .reply(200, response); + + const result = await ProtectedApiClient.filterSiteImages(params); + + expect(result).toEqual(response); + }); + + it('makes a bad request', async () => { + const response = 'Invalid dates given!'; + + const params = { + submittedStart: 'invalid date', + submittedEnd: null, + neighborhoods: [0, 1, 2], + siteIds: [0, 1, 2, 3], + }; + + nock(BASE_URL) + .get(ParameterizedAdminApiRoutes.FILTER_SITE_IMAGES(params)) + .reply(400, response); + + const result = await ProtectedApiClient.filterSiteImages(params).catch( + (err) => err.response.data, + ); + + expect(result).toEqual(response); + }); + + it('makes an unauthorized request ', async () => { + const response = 'Must be an admin'; + + const params = { + submittedStart: null, + submittedEnd: null, + neighborhoods: [0, 1, 2], + siteIds: [0, 1, 2, 3], + }; + + nock(BASE_URL) + .get(ParameterizedAdminApiRoutes.FILTER_SITE_IMAGES(params)) + .reply(401, response); + + const result = await ProtectedApiClient.filterSiteImages(params).catch( + (err) => err.response.data, + ); + + expect(result).toEqual(response); + }); + }); + describe('uploadImage', () => { let imageToUpload: string | ArrayBuffer; diff --git a/src/components/imageApprovalModal/index.tsx b/src/components/imageApprovalModal/index.tsx new file mode 100644 index 00000000..4f4f438e --- /dev/null +++ b/src/components/imageApprovalModal/index.tsx @@ -0,0 +1,259 @@ +import { Modal, Space, Button, Row, Col, Input, message } from 'antd'; +import React, { + CSSProperties, + Dispatch, + SetStateAction, + useEffect, + useState, +} from 'react'; +import { LeftOutlined, RightOutlined } from '@ant-design/icons'; +import { FilterImageTableData } from '../../containers/reviewImages/types'; +import protectedApiClient from '../../api/protectedApiClient'; +import { useTranslation } from 'react-i18next'; +import { n } from '../../utils/stringFormat'; +import { site } from '../../constants'; +const { TextArea } = Input; + +interface ImageApprovalModal { + visible: boolean; + onClose: () => void; + tableData: FilterImageTableData | null; + approvedOrRejectedImageIds: number[]; + setApprovedOrRejectedImageIds: Dispatch>; + allData: FilterImageTableData[]; + setSelectedImage: Dispatch>; +} + +const ImageApprovalModal: React.FC = (props) => { + const { t } = useTranslation(n(site, ['admin']), { + nsMode: 'fallback', + keyPrefix: 'review_images', + }); + + const { t: tForms } = useTranslation(n(site, ['forms']), { + nsMode: 'fallback', + }); + + const [open, setOpen] = useState(props.visible); + const [data, setData] = useState(props.tableData); + const [isRejectionTextOpen, setIsRejectionTextOpen] = useState(false); + const [rejectionReason, setRejectionReason] = useState(''); + const [allData, setAllData] = useState(props.allData); + + const close = () => { + setOpen(false); + props.onClose(); + }; + + const openRejectionTextBox = () => { + setIsRejectionTextOpen(true); + }; + + const handleNextSubmission = () => { + const currentIndex = allData.findIndex( + (siteData) => siteData.key === data?.key, + ); + const nextIndex = (currentIndex + 1) % allData.length; + const nextImage = allData[nextIndex]; + setData(nextImage); + }; + + const footer = ( + + + + + ); + + const rejectionReasonStyle = { + background: 'rgb(239, 239, 239)', + borderRadius: '5px', + marginTop: '5%', + padding: '3%', + paddingRight: '10%', + }; + + const StatusHeader = () => { + return ( + <> +

+ {t('modal.status')} +

+
+ + +
+ + ); + }; + + if (!data) { + return null; + } + + const treeSummaryTextStyle: CSSProperties = { + whiteSpace: 'nowrap', // Prevent text from wrapping + }; + + async function onClickReject() { + const toReject: Promise[] = []; + if (!data) { + return null; + } + toReject.push(protectedApiClient.rejectImage(data.key, rejectionReason)); + Promise.all(toReject) + .then(() => { + props.setApprovedOrRejectedImageIds((prevIds) => [ + ...prevIds, + data.key, + ]); + close(); + }) + .then(() => { + message.success(t('message.reject_success')); + }) + .catch((err) => { + message.error(t('message.reject_error', { error: err.response.data })); + }); + } + + async function onClickAccept() { + const toApprove: Promise[] = []; + if (!data) { + return null; + } + toApprove.push(protectedApiClient.approveImage(data.key)); + Promise.all(toApprove) + .then(() => { + props.setApprovedOrRejectedImageIds((prevIds) => [ + ...prevIds, + data.key, + ]); + close(); + }) + .then(() => { + message.success(t('message.approve_success')); + }) + .catch((err) => { + message.error(t('message.approve_error', { error: err.response.data })); + }); + } + + function treeSummaryLine(lineName: string, lineItem: number | string) { + return ( + <> + + +

{lineName}

+
+ + +

{lineItem}

+ + + ); + } + + const TreeSummaryDisplay = () => { + return ( +
+ + + + {isRejectionTextOpen ? ( + <> + ) : ( + <> + {treeSummaryLine(t('summary.id'), data.siteId)} + {treeSummaryLine(t('summary.date'), data.dateSubmitted)} + {treeSummaryLine(t('summary.submitted'), data.submittedBy)} + + )} + {treeSummaryLine(t('summary.species'), data.species)} + {treeSummaryLine(t('summary.neighborhood'), data.neighborhood)} + {treeSummaryLine(t('summary.address'), data.address)} + + + +
+ ); + }; + + useEffect(() => { + setOpen(open); + }, [props.visible]); + + return ( + + + + Submission + + + + {isRejectionTextOpen ? ( +
+

{t('modal.form.prompt')}

+