diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 4bf7b8468..3e57b10a1 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -317,9 +317,9 @@ export class RequestsService { validateId(requestId, 'Request'); if ( - dto.requestedSize == undefined && - dto.requestedFoodTypes == undefined && - dto.additionalInformation == undefined + dto.requestedSize === undefined && + dto.requestedFoodTypes === undefined && + dto.additionalInformation === undefined ) { throw new BadRequestException( 'At least one field must be provided to update request', diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 5e822dfb1..3755ab257 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -43,6 +43,7 @@ import { BulkUpdateTrackingCostDto, UpdateDonationItemDetailsDto, PendingApplication, + UpdateFoodRequestBody, DonationReminderDto, } from 'types/types'; @@ -104,6 +105,17 @@ export class ApiClient { .then((response) => response.data); } + public async updateFoodRequest( + requestId: number, + body: UpdateFoodRequestBody, + ): Promise { + await this.axiosInstance.patch(`/api/requests/${requestId}`, body); + } + + public async deleteFoodRequest(requestId: number): Promise { + await this.axiosInstance.delete(`/api/requests/${requestId}`); + } + public async closeFoodRequest(requestId: number): Promise { await this.axiosInstance.patch(`/api/requests/${requestId}/close`, {}); } diff --git a/apps/frontend/src/components/editDeleteButtons.tsx b/apps/frontend/src/components/editDeleteButtons.tsx new file mode 100644 index 000000000..63133a39a --- /dev/null +++ b/apps/frontend/src/components/editDeleteButtons.tsx @@ -0,0 +1,50 @@ +import { HStack } from '@chakra-ui/react'; +import { Pencil, Trash2 } from 'lucide-react'; + +interface EditDeleteButtonProps { + onClick: () => void; +} + +export const EditButton: React.FC = ({ onClick }) => { + return ( + + + + ); +}; + +export const DeleteButton: React.FC = ({ onClick }) => { + return ( + + + + ); +}; diff --git a/apps/frontend/src/components/foodRequestManagement.tsx b/apps/frontend/src/components/foodRequestManagement.tsx index 7d0bce590..852f16ef6 100644 --- a/apps/frontend/src/components/foodRequestManagement.tsx +++ b/apps/frontend/src/components/foodRequestManagement.tsx @@ -16,6 +16,7 @@ import { capitalize, formatDate } from '@utils/utils'; import { FloatingAlert } from '@components/floatingAlert'; import { FoodRequestStatus, FoodRequestSummaryDto } from '../types/types'; import RequestDetailsModal from '@components/forms/requestDetailsModal'; +import PantryDeleteRequestActionModal from '@components/forms/pantryDeleteRequestModal'; import VolunteerCloseRequestActionModal from '@components/forms/volunteerCloseRequestModal'; import VolunteerRequestActionRequiredModal from '@components/forms/volunteerRequestActionRequiredModal'; import CreateNewOrderModal from '@components/forms/createNewOrderModal'; @@ -43,6 +44,8 @@ const RequestManagement: React.FC = ({ >([]); const [selectedViewDetailsRequest, setSelectedViewDetailsRequest] = useState(null); + const [deleteRequest, setDeleteRequest] = + useState(null); const [selectedActionRequest, setSelectedActionRequest] = useState(null); const [selectedCloseRequestAction, setSelectedCloseRequestAction] = @@ -402,6 +405,21 @@ const RequestManagement: React.FC = ({ navigate(location.pathname, { replace: true }); } }} + onSuccess={loadRequests} + onDelete={() => setDeleteRequest(selectedViewDetailsRequest)} + /> + )} + + {deleteRequest && ( + setDeleteRequest(null)} + onSuccess={() => { + setSuccessMessage('Successfully deleted food request.'); + loadRequests(); + setSelectedViewDetailsRequest(null); + }} /> )} diff --git a/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx b/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx new file mode 100644 index 000000000..33851e4b3 --- /dev/null +++ b/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx @@ -0,0 +1,125 @@ +import React from 'react'; +import { + Box, + Button, + VStack, + CloseButton, + Text, + Flex, + Dialog, +} from '@chakra-ui/react'; +import { FoodRequestSummaryDto } from 'types/types'; +import { formatDate } from '@utils/utils'; +import apiClient from '@api/apiClient'; +import { useAlert } from '../../hooks/alert'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useModalBodyCleanup } from '../../hooks/modalBodyCleanup'; + +interface DeleteRequestActionModalProps { + request: FoodRequestSummaryDto; + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; +} + +const PantryDeleteRequestActionModal: React.FC< + DeleteRequestActionModalProps +> = ({ request, isOpen, onClose, onSuccess }) => { + useModalBodyCleanup(); + const [alertState, setAlertMessage] = useAlert(); + + const onCloseRequest = async () => { + try { + await apiClient.deleteFoodRequest(request.requestId); + onClose(); + onSuccess(); + } catch { + setAlertMessage('Food request could not be deleted.'); + } + }; + + return ( + { + if (!e.open) onClose(); + }} + closeOnInteractOutside + > + {alertState && ( + + )} + + + + + + + + + + Confirm Action + + + + + + Are you sure you want to delete this food request? This action + cannot be undone. + + + + Request #{request.requestId} + + + Submitted {formatDate(request.requestedAt)} + + + + + + + + + + + + ); +}; + +export default PantryDeleteRequestActionModal; diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index 23c4e9e80..9a22156de 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -3,8 +3,15 @@ import { OrderItemDetailsGroupedByFoodType, OrderDetails, FoodRequestSummaryDto, -} from 'types/types'; -import { OrderStatus } from '../../types/types'; + FoodRequestStatus, +} from '../../types/types'; +import { + OrderStatus, + RequestSize, + FoodType, + User, + Role, +} from '../../types/types'; import { ORDER_STATUS_LABELS } from '@utils/utils'; import React, { useState, useEffect } from 'react'; import { @@ -20,30 +27,62 @@ import { Pagination, ButtonGroup, IconButton, + Button, + Textarea, } from '@chakra-ui/react'; -import { ChevronRight, ChevronLeft } from 'lucide-react'; +import { ChevronRight, ChevronLeft, ChevronDownIcon } from 'lucide-react'; import { TagGroup } from './tagGroup'; import { useGroupedItemsByFoodType } from '../../hooks/groupedItemsByFoodType'; import { useModalBodyCleanup } from '../../hooks/modalBodyCleanup'; +import { useAlert } from '../../hooks/alert'; +import { FloatingAlert } from '../floatingAlert'; +import { useAuthenticator } from '@aws-amplify/ui-react'; +import { EditButton, DeleteButton } from '@components/editDeleteButtons'; interface RequestDetailsModalProps { request: FoodRequestSummaryDto; isOpen: boolean; onClose: () => void; + onSuccess: () => void; + onDelete: () => void; } const RequestDetailsModal: React.FC = ({ request, isOpen, onClose, + onSuccess, + onDelete, }) => { useModalBodyCleanup(); + const { authStatus } = useAuthenticator((context) => [context.authStatus]); + const [currentUser, setCurrentUser] = useState(null); + + const [alertState, setAlertMessage] = useAlert(); + const [orderDetailsList, setOrderDetailsList] = useState([]); const [currentPage, setCurrentPage] = useState(1); - const requestedSize = request.requestedSize; - const selectedFoodTypes = request.requestedFoodTypes; - const additionalNotes = request.additionalInformation; + const [requestedSize, setRequestedSize] = useState( + request.requestedSize, + ); + const [selectedFoodTypes, setSelectedFoodTypes] = useState( + request.requestedFoodTypes, + ); + const [additionalNotes, setAdditionalNotes] = useState( + request.additionalInformation ?? '', + ); + + useEffect(() => { + if (authStatus === 'authenticated') { + apiClient + .getMe() + .then(setCurrentUser) + .catch(() => setCurrentUser(null)); + } else { + setCurrentUser(null); + } + }, [authStatus]); useEffect(() => { const fetchRequestOrderDetails = async () => { @@ -88,240 +127,457 @@ const RequestDetailsModal: React.FC = ({ fontWeight: '500', }; + const [isEditing, setIsEditing] = useState(false); + + const handleCancel = () => { + setRequestedSize(request.requestedSize); + setSelectedFoodTypes(request.requestedFoodTypes); + setAdditionalNotes(request.additionalInformation ?? ''); + setIsEditing(false); + }; + + const handleUpdate = async () => { + try { + await apiClient.updateFoodRequest(request.requestId, { + requestedSize, + requestedFoodTypes: selectedFoodTypes, + additionalInformation: additionalNotes === '' ? null : additionalNotes, + }); + onSuccess(); + setAlertMessage('Successfully updated food request.'); + setIsEditing(false); + } catch { + setAlertMessage('Food request could not be updated.'); + } + }; + return ( - { - if (!e.open) onClose(); - }} - closeOnInteractOutside - > - - - - - - Food Request #{request.requestId} - - - - - {pantryName} - + <> + {/* + * TODO: hard-coded as 'info' for now because I worked on another ticket that refactored alertState to have a status + */} + {alertState && ( + + )} + { + if (!e.open) onClose(); + }} + closeOnInteractOutside + > + + + + + + Food Request #{request.requestId} + + {!isEditing && request.status === FoodRequestStatus.ACTIVE && ( + <> + {currentUser?.role === Role.PANTRY && ( + setIsEditing(true)}> + )} + + + )} + + + + {pantryName} + + {isEditing ? ( + <> + + + + Size of Shipment + + + + + + + + + + setRequestedSize(val.value) + } + > + {Object.values(RequestSize).map((option, idx) => ( + + {option} + + ))} + + + + + - - - - Request Details - - - Associated Orders - - - - - - Size of Shipment - - - - {requestedSize} - - - + + + + Food Type(s) + + + + + + + + + {Object.values(FoodType).map((allergen) => { + const isChecked = + selectedFoodTypes.includes(allergen); + return ( + + setSelectedFoodTypes((prev) => + checked + ? [...prev, allergen as FoodType] + : prev.filter((i) => i !== allergen), + ) + } + display="flex" + alignItems="center" + > + + + + {allergen} + + + ); + })} + + + + + setSelectedFoodTypes((prev) => + prev.filter((i) => i !== value), + ) + } + /> + - - - - Food Type(s) - - + + + + Additional Information + + +