From 1a7e2edda65d467100b093b1e373cd5d3e7c0ac9 Mon Sep 17 00:00:00 2001 From: Heidi Jiang Date: Sat, 30 May 2026 23:50:34 -0700 Subject: [PATCH 1/5] added edit/delete buttons + edit view/delete modal + connected to backend --- apps/frontend/src/api/apiClient.ts | 12 + .../src/components/foodRequestManagement.tsx | 1 + .../forms/pantryDeleteRequestModal.tsx | 133 ++++ .../components/forms/requestDetailsModal.tsx | 730 ++++++++++++------ apps/frontend/src/containers/formRequests.tsx | 1 + apps/frontend/src/types/types.ts | 6 + 6 files changed, 665 insertions(+), 218 deletions(-) create mode 100644 apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index ff731e2e2..042fe3c96 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -44,6 +44,7 @@ import { BulkUpdateTrackingCostDto, UpdateDonationItemDetailsDto, PendingApplication, + UpdateFoodRequestBody, } from 'types/types'; const defaultBaseUrl = @@ -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/foodRequestManagement.tsx b/apps/frontend/src/components/foodRequestManagement.tsx index 1db69e7df..0b6091f16 100644 --- a/apps/frontend/src/components/foodRequestManagement.tsx +++ b/apps/frontend/src/components/foodRequestManagement.tsx @@ -392,6 +392,7 @@ const RequestManagement: React.FC = ({ navigate(location.pathname, { replace: true }); } }} + onSuccess={loadRequests} /> )} diff --git a/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx b/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx new file mode 100644 index 000000000..9c8c431e7 --- /dev/null +++ b/apps/frontend/src/components/forms/pantryDeleteRequestModal.tsx @@ -0,0 +1,133 @@ +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('Error completing action. Please try again.'); + } + }; + + const cancelButton = ( + + ); + + const deleteButton = ( + + ); + + 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)} + + + + {cancelButton} + {deleteButton} + + + + + + + ); +}; + +export default PantryDeleteRequestActionModal; diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index 23c4e9e80..aea9dbae6 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -3,8 +3,9 @@ import { OrderItemDetailsGroupedByFoodType, OrderDetails, FoodRequestSummaryDto, + UpdateFoodRequestBody, } from 'types/types'; -import { OrderStatus } from '../../types/types'; +import { OrderStatus, RequestSize, FoodType } from '../../types/types'; import { ORDER_STATUS_LABELS } from '@utils/utils'; import React, { useState, useEffect } from 'react'; import { @@ -20,30 +21,48 @@ import { Pagination, ButtonGroup, IconButton, + HStack, + Button, + Textarea, } from '@chakra-ui/react'; -import { ChevronRight, ChevronLeft } from 'lucide-react'; +import { + ChevronRight, + ChevronLeft, + Pencil, + Trash2, + ChevronDownIcon, +} from 'lucide-react'; import { TagGroup } from './tagGroup'; import { useGroupedItemsByFoodType } from '../../hooks/groupedItemsByFoodType'; import { useModalBodyCleanup } from '../../hooks/modalBodyCleanup'; +import PantryDeleteRequestActionModal from './pantryDeleteRequestModal'; interface RequestDetailsModalProps { request: FoodRequestSummaryDto; isOpen: boolean; onClose: () => void; + onSuccess: () => void; } const RequestDetailsModal: React.FC = ({ request, isOpen, onClose, + onSuccess, }) => { useModalBodyCleanup(); 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(() => { const fetchRequestOrderDetails = async () => { @@ -88,240 +107,515 @@ const RequestDetailsModal: React.FC = ({ fontWeight: '500', }; - return ( - { - if (!e.open) onClose(); - }} - closeOnInteractOutside + const [isEditing, setIsEditing] = useState(false); + const [selectedDeleteRequestAction, setSelectedDeleteRequestAction] = + useState(false); + + const handleCancel = () => { + setRequestedSize(request.requestedSize); + setSelectedFoodTypes(request.requestedFoodTypes); + setAdditionalNotes(request.additionalInformation ?? ''); + setIsEditing(false); + }; + + const handleUpdate = async () => { + const changed: UpdateFoodRequestBody = {}; + if (requestedSize !== request.requestedSize) + changed.requestedSize = requestedSize; + const foodTypesChanged = + selectedFoodTypes.length !== request.requestedFoodTypes.length || + !selectedFoodTypes.every((t) => request.requestedFoodTypes.includes(t)); + if (foodTypesChanged) changed.requestedFoodTypes = selectedFoodTypes; + if (additionalNotes !== (request.additionalInformation ?? '')) + changed.additionalInformation = additionalNotes; + + await apiClient.updateFoodRequest(request.requestId, changed); + onSuccess(); + setIsEditing(false); + }; + + const editButton = ( + setIsEditing(true)} > - - - - - - Food Request #{request.requestId} - - - - - {pantryName} - + + + ); - - - - Request Details - - - Associated Orders - - - - - - Size of Shipment - - - - {requestedSize} - - - + const deleteButton = ( + setSelectedDeleteRequestAction(true)} + > + + + ); - - - - Food Type(s) - - + return ( + <> + { + if (!e.open) onClose(); + }} + closeOnInteractOutside + > + + + + + + Food Request #{request.requestId} + + {!isEditing && ( + <> + {editButton} + {deleteButton} + + )} + + + + {pantryName} + + {isEditing ? ( + <> + + + + Size of Shipment + + + + + + + + + + setRequestedSize(val.value) + } + > + {Object.values(RequestSize).map((option, idx) => ( + + {option} + + ))} + + + + + - - + + + + 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), + ) + } + /> + - - - - Additional Information - - - - {additionalNotes} - - - + + + + Additional Information + + +