diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index 075fb1452..13a47e6df 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -20,8 +20,8 @@ import { CreateMultipleDonationItemsDto } from './dtos/create-donation-items.dto export class DonationItemsController { constructor(private donationItemsService: DonationItemsService) {} - @Get('/get-donation-items/:donationId') - async getAllDonationIdItems( + @Get('/:donationId/all') + async getAllDonationItemsForDonation( @Param('donationId', ParseIntPipe) donationId: number, ): Promise { return this.donationItemsService.getAllDonationItems(donationId); diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 26991a8bb..37e4ed11f 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -24,6 +24,7 @@ import { OrderDetails, Assignments, FoodRequestSummaryDto, + PantryWithUser, } from 'types/types'; const defaultBaseUrl = @@ -156,7 +157,7 @@ export class ApiClient { return this.get(`/api/pantries/${pantryId}/ssf-contact`) as Promise; } - public async getAllPendingPantries(): Promise { + public async getAllPendingPantries(): Promise { return this.axiosInstance .get('/api/pantries/pending') .then((response) => response.data); @@ -168,8 +169,8 @@ export class ApiClient { .then((response) => response.data); } - public async getPantry(pantryId: number): Promise { - return this.get(`/api/pantries/${pantryId}`) as Promise; + public async getPantry(pantryId: number): Promise { + return this.get(`/api/pantries/${pantryId}`) as Promise; } public async postPantry( @@ -212,9 +213,9 @@ export class ApiClient { public async getDonationItemsByDonationId( donationId: number, ): Promise { - return this.get( - `/api/donation-items/get-donation-items/${donationId}`, - ) as Promise; + return this.get(`/api/donation-items/${donationId}/all`) as Promise< + DonationItem[] + >; } public async getManufacturerFromOrder( diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index ecedbf432..d679ca6e4 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -11,6 +11,7 @@ import PantryApplication from '@containers/pantryApplication'; import ApplicationSubmitted from '@containers/applicationSubmitted'; import { submitPantryApplicationForm } from '@components/forms/pantryApplicationForm'; import ApprovePantries from '@containers/approvePantries'; +import PantryApplicationDetails from '@containers/pantryApplicationDetails'; import VolunteerManagement from '@containers/volunteerManagement'; import FoodManufacturerOrderDashboard from '@containers/foodManufacturerOrderDashboard'; import AdminDonation from '@containers/adminDonation'; @@ -146,41 +147,49 @@ const router = createBrowserRouter([ ), }, { - path: '/volunteer-management', + path: '/approve-pantries', element: ( - + ), }, { - path: '/admin-order-management', + path: '/pantry-application-details/:applicationId', element: ( - + ), }, { - path: '/confirm-delivery', - action: submitDeliveryConfirmationFormModal, + path: '/admin-donation', + element: ( + + + + ), }, { - path: '/approve-pantries', + path: '/volunteer-management', element: ( - + ), }, { - path: '/admin-donation', + path: '/admin-order-management', element: ( - + ), }, + { + path: '/confirm-delivery', + action: submitDeliveryConfirmationFormModal, + }, { path: '/volunteer-assigned-pantries', element: ( diff --git a/apps/frontend/src/components/floatingAlert.tsx b/apps/frontend/src/components/floatingAlert.tsx index 0257b0b36..36ca0b85b 100644 --- a/apps/frontend/src/components/floatingAlert.tsx +++ b/apps/frontend/src/components/floatingAlert.tsx @@ -49,7 +49,7 @@ export function FloatingAlert({ p={2} > - + {message} diff --git a/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx b/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx new file mode 100644 index 000000000..29b5329f7 --- /dev/null +++ b/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx @@ -0,0 +1,88 @@ +import { Dialog, Text, Box, Button, CloseButton } from '@chakra-ui/react'; +import { capitalize } from '@utils/utils'; + +interface ConfirmPantryDecisionModalProps { + isOpen: boolean; + onClose: () => void; + onConfirm: () => void; + decision: string; + pantryName: string; + dateApplied: string; +} + +const ConfirmPantryDecisionModal: React.FC = ({ + isOpen, + onClose, + onConfirm, + decision, + pantryName, + dateApplied, +}) => { + return ( + !e.open && onClose()} + > + + + + + + Confirm Action + + + + + + Are you sure you want to {decision} this application? + + + + {pantryName} + + Applied {dateApplied} + + + + + + + + + + + + + + + ); +}; + +export default ConfirmPantryDecisionModal; diff --git a/apps/frontend/src/components/forms/donationDetailsModal.tsx b/apps/frontend/src/components/forms/donationDetailsModal.tsx index 389fd2537..0aa3cd5a4 100644 --- a/apps/frontend/src/components/forms/donationDetailsModal.tsx +++ b/apps/frontend/src/components/forms/donationDetailsModal.tsx @@ -11,6 +11,7 @@ import ApiClient from '@api/apiClient'; import { Donation, DonationItem, FoodType } from 'types/types'; import { formatDate } from '@utils/utils'; import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../../hooks/alert'; interface DonationDetailsModalProps { donation: Donation; @@ -25,7 +26,7 @@ const DonationDetailsModal: React.FC = ({ }) => { const [items, setItems] = useState([]); - const [alertMessage, setAlertMessage] = useState(''); + const [alertState, setAlertMessage] = useAlert(); const donationId = donation.donationId; @@ -39,13 +40,13 @@ const DonationDetailsModal: React.FC = ({ ); setItems(itemsData); - } catch (err) { - setAlertMessage('Error fetching donation details: ' + err); + } catch { + setAlertMessage('Error fetching donation details'); } }; fetchData(); - }, [isOpen, donationId]); + }, [isOpen, donationId, setAlertMessage]); // Group items by food type const groupedItems = items.reduce((acc, item) => { @@ -63,8 +64,13 @@ const DonationDetailsModal: React.FC = ({ closeOnInteractOutside scrollBehavior="inside" > - {alertMessage && ( - + {alertState && ( + )} diff --git a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx index 38c59bcb1..3350edfb7 100644 --- a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx +++ b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx @@ -20,8 +20,9 @@ import { ActionFunctionArgs, Form, redirect, + useActionData, } from 'react-router-dom'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { USPhoneInput } from '@components/forms/usPhoneInput'; import { TagGroup } from '@components/forms/tagGroup'; import { ManufacturerApplicationDto } from '../../types/types'; @@ -33,6 +34,8 @@ import { DonateWastedFood, ManufacturerAttribute, } from '../../types/manufacturerEnums'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../../hooks/alert'; const ManufacturerApplicationForm: React.FC = () => { const [contactPhone, setContactPhone] = useState(''); @@ -44,6 +47,8 @@ const ManufacturerApplicationForm: React.FC = () => { const [facilityFreeAllergens, setFacilityFreeAllergens] = useState< Allergen[] >([]); + const [alertState, setAlertMessage] = useAlert(); + const actionData = useActionData() as { error?: string } | undefined; const sectionTitleStyles = { fontFamily: 'inter', @@ -68,9 +73,23 @@ const ManufacturerApplicationForm: React.FC = () => { fontWeight: '600', }; + useEffect(() => { + if (actionData?.error) { + setAlertMessage(actionData.error); + } + }, [actionData, setAlertMessage]); + return ( + {alertState && ( + + )} Partner Manufacturer Application @@ -699,28 +718,27 @@ export const submitManufacturerApplicationForm: ActionFunction = async ({ }); const data = Object.fromEntries(manufacturerApplicationData); - let submissionSuccessful = false; - await ApiClient.postManufacturer(data as ManufacturerApplicationDto).then( - () => (submissionSuccessful = true), - (error) => { - if (axios.isAxiosError(error) && error.response?.status === 400) { - alert( + try { + await ApiClient.postManufacturer(data as ManufacturerApplicationDto); + return redirect('/application-submitted'); + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 400) { + return { + error: 'Form submission failed with the following errors: \n\n' + - // Creates a bullet-point list of the errors - // returned from the backend - error.response?.data?.message - .map((line: string) => '- ' + line) - .join('\n'), - ); - } else { - alert('Form submission failed; please try again'); - console.log(error); - } - }, - ); - - return submissionSuccessful ? redirect('/application-submitted') : null; + // Creates a bullet-point list of the errors + // returned from the backend + error.response?.data?.message + .map((line: string) => '- ' + line) + .join('\n'), + }; + } else { + return { + error: 'Form submission failed; please try again', + }; + } + } }; export default ManufacturerApplicationForm; diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index 5d11c90c0..a6ef68505 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -25,6 +25,8 @@ import { } from '../../types/types'; import { Minus } from 'lucide-react'; import { generateNextDonationDate } from '@utils/utils'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../../hooks/alert'; interface NewDonationFormModalProps { onDonationSuccess: () => void; @@ -86,6 +88,7 @@ const NewDonationFormModal: React.FC = ({ const [totalItems, setTotalItems] = useState(0); const [totalOz, setTotalOz] = useState(0); const [totalValue, setTotalValue] = useState(0); + const [alertState, setAlertMessage] = useAlert(); const handleChange = (id: number, field: string, value: string | boolean) => { const updatedRows = rows.map((row) => @@ -169,7 +172,7 @@ const NewDonationFormModal: React.FC = ({ (row) => !row.foodItem || !row.foodType || !row.numItems, ); if (hasEmpty) { - alert('Please fill in all fields before submitting.'); + setAlertMessage('Please fill in all fields before submitting.'); return; } @@ -178,7 +181,7 @@ const NewDonationFormModal: React.FC = ({ repeatInterval === RecurrenceEnum.WEEKLY && !Object.values(repeatOn).some(Boolean) ) { - alert('Please select at least one day for weekly recurrence.'); + setAlertMessage('Please select at least one day for weekly recurrence.'); return; } @@ -198,7 +201,6 @@ const NewDonationFormModal: React.FC = ({ try { const donationResponse = await ApiClient.postDonation(donation_body); - console.log('Submitted donation'); const donationId = donationResponse?.donationId; if (donationId) { @@ -233,10 +235,10 @@ const NewDonationFormModal: React.FC = ({ setRepeatInterval(RecurrenceEnum.NONE); onClose(); } else { - alert('Failed to submit donation'); + setAlertMessage('Failed to submit donation'); } - } catch (error) { - alert('Error submitting new donation: ' + error); + } catch { + setAlertMessage('Error submitting new donation'); } }; @@ -258,6 +260,14 @@ const NewDonationFormModal: React.FC = ({ }} closeOnInteractOutside > + {alertState && ( + + )} diff --git a/apps/frontend/src/components/forms/orderDetailsModal.tsx b/apps/frontend/src/components/forms/orderDetailsModal.tsx index 44bdc8915..235153234 100644 --- a/apps/frontend/src/components/forms/orderDetailsModal.tsx +++ b/apps/frontend/src/components/forms/orderDetailsModal.tsx @@ -21,6 +21,7 @@ import { FoodRequestStatus } from '../../types/types'; import { TagGroup } from './tagGroup'; import { useGroupedItemsByFoodType } from '../../hooks/groupedItemsByType'; import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../../hooks/alert'; interface OrderDetailsModalProps { orderId: number; @@ -38,7 +39,7 @@ const OrderDetailsModal: React.FC = ({ ); const [orderDetails, setOrderDetails] = useState(null); - const [alertMessage, setAlertMessage] = useState(''); + const [alertState, setAlertMessage] = useAlert(); useEffect(() => { if (isOpen) { @@ -48,14 +49,14 @@ const OrderDetailsModal: React.FC = ({ orderId, ); setFoodRequest(foodRequestData); - } catch (error) { - setAlertMessage('Error fetching food request details:' + error); + } catch { + setAlertMessage('Error fetching food request details'); } }; fetchRequestData(); } - }, [isOpen, orderId]); + }, [isOpen, orderId, setAlertMessage]); useEffect(() => { if (isOpen) { @@ -63,14 +64,14 @@ const OrderDetailsModal: React.FC = ({ try { const orderDetailsData = await ApiClient.getOrder(orderId); setOrderDetails(orderDetailsData); - } catch (error) { - setAlertMessage('Error fetching order details:' + error); + } catch { + setAlertMessage('Error fetching order details'); } }; fetchOrderDetails(); } - }, [isOpen, orderId]); + }, [isOpen, orderId, setAlertMessage]); const groupedOrderItemsByType: GroupedByFoodType = useGroupedItemsByFoodType( orderDetails?.items, @@ -99,8 +100,13 @@ const OrderDetailsModal: React.FC = ({ }} closeOnInteractOutside > - {alertMessage && ( - + {alertState && ( + )} diff --git a/apps/frontend/src/components/forms/pantryApplicationForm.tsx b/apps/frontend/src/components/forms/pantryApplicationForm.tsx index f75217e16..bf133a87b 100644 --- a/apps/frontend/src/components/forms/pantryApplicationForm.tsx +++ b/apps/frontend/src/components/forms/pantryApplicationForm.tsx @@ -20,8 +20,9 @@ import { ActionFunctionArgs, Form, redirect, + useActionData, } from 'react-router-dom'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { USPhoneInput } from '@components/forms/usPhoneInput'; import { PantryApplicationDto } from '../../types/types'; import ApiClient from '@api/apiClient'; @@ -29,6 +30,8 @@ import { Activity } from '../../types/pantryEnums'; import axios from 'axios'; import { ChevronDownIcon } from 'lucide-react'; import { TagGroup } from './tagGroup'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../../hooks/alert'; const otherRestrictionsOptions: string[] = [ 'Other allergy (e.g., yeast, sunflower, etc.)', @@ -79,6 +82,8 @@ const PantryApplicationForm: React.FC = () => { boolean | null >(); const [otherEmailContact, setOtherEmailContact] = useState(false); + const [alertState, setAlertMessage] = useAlert(); + const actionData = useActionData() as { error?: string } | undefined; const sectionTitleStyles = { fontFamily: 'inter', @@ -103,9 +108,23 @@ const PantryApplicationForm: React.FC = () => { fontWeight: '600', }; + useEffect(() => { + if (actionData?.error) { + setAlertMessage(actionData.error); + } + }, [actionData, setAlertMessage]); + return ( + {alertState && ( + + )} Partner Pantry Application @@ -1189,27 +1208,26 @@ export const submitPantryApplicationForm: ActionFunction = async ({ const data = Object.fromEntries(pantryApplicationData); - let submissionSuccessful = false; - - await ApiClient.postPantry(data as PantryApplicationDto).then( - () => (submissionSuccessful = true), - (error) => { - if (axios.isAxiosError(error) && error.response?.status === 400) { - alert( + try { + await ApiClient.postPantry(data as PantryApplicationDto); + return redirect('/application-submitted'); + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 400) { + return { + error: 'Form submission failed with the following errors: \n\n' + - // Creates a bullet-point list of the errors - // returned from the backend - error.response?.data?.message - .map((line: string) => '- ' + line) - .join('\n'), - ); - } else { - alert('Form submission failed; please try again'); - } - }, - ); - - return submissionSuccessful ? redirect('/application-submitted') : null; + // Creates a bullet-point list of the errors + // returned from the backend + error.response?.data?.message + .map((line: string) => '- ' + line) + .join('\n'), + }; + } else { + return { + error: 'Form submission failed; please try again', + }; + } + } }; export default PantryApplicationForm; diff --git a/apps/frontend/src/components/forms/pantryApplicationModal.tsx b/apps/frontend/src/components/forms/pantryApplicationModal.tsx index 4c973fd67..369492227 100644 --- a/apps/frontend/src/components/forms/pantryApplicationModal.tsx +++ b/apps/frontend/src/components/forms/pantryApplicationModal.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { Button, Dialog, Grid, GridItem, Text } from '@chakra-ui/react'; -import { Pantry } from 'types/types'; +import { PantryWithUser } from 'types/types'; interface PantryApplicationModalProps { - pantry: Pantry; + pantry: PantryWithUser; isOpen: boolean; onClose: () => void; } @@ -66,32 +66,32 @@ const PantryApplicationModal: React.FC = ({ Shipping Address Line 1 - {pantry.shippingAddressLine1} + {pantry.shipmentAddressLine1} Shipping Address Line 2 - {pantry.shippingAddressLine2 ?? ''} + {pantry.shipmentAddressLine2 ?? ''} Shipping Address City - {pantry.shippingAddressCity} + {pantry.shipmentAddressCity} Shipping Address State - {pantry.shippingAddressState} + {pantry.shipmentAddressState} Shipping Address Zip - {pantry.shippingAddressZip} + {pantry.shipmentAddressZip} Shipping Address Country - {pantry.shippingAddressCountry ?? ''} + {pantry.shipmentAddressCountry ?? ''} Allergen Clients diff --git a/apps/frontend/src/components/forms/requestFormModal.tsx b/apps/frontend/src/components/forms/requestFormModal.tsx index 575912d28..1e1d1ad37 100644 --- a/apps/frontend/src/components/forms/requestFormModal.tsx +++ b/apps/frontend/src/components/forms/requestFormModal.tsx @@ -40,9 +40,11 @@ const FoodRequestFormModal: React.FC = ({ const [requestedSize, setRequestedSize] = useState(''); const [additionalNotes, setAdditionalNotes] = useState(''); const [alert, setAlert] = useState<{ + key: number; isError: boolean; message: string; }>({ + key: 0, isError: true, message: '', }); @@ -70,11 +72,19 @@ const FoodRequestFormModal: React.FC = ({ try { await apiClient.createFoodRequest(foodRequestData); - setAlert({ isError: false, message: 'Request Submitted' }); + setAlert((prev) => ({ + key: prev.key + 1, + isError: false, + message: 'Request submitted', + })); onClose(); onSuccess(); } catch { - setAlert({ isError: true, message: 'Request could not be submitted.' }); + setAlert((prev) => ({ + key: prev.key + 1, + isError: true, + message: 'Request could not be submitted.', + })); } }; @@ -88,10 +98,20 @@ const FoodRequestFormModal: React.FC = ({ closeOnInteractOutside > {alert.message && alert.isError && ( - + )} {alert.message && !alert.isError && ( - + )} @@ -270,10 +290,11 @@ const FoodRequestFormModal: React.FC = ({ if (words.length <= 250) { setAdditionalNotes(e.target.value); } else { - setAlert({ + setAlert((prev) => ({ + key: prev.key + 1, isError: true, message: 'Exceeded word limit', - }); + })); } }} /> diff --git a/apps/frontend/src/components/forms/resetPasswordModal.tsx b/apps/frontend/src/components/forms/resetPasswordModal.tsx index 8649a9174..fa7f61b52 100644 --- a/apps/frontend/src/components/forms/resetPasswordModal.tsx +++ b/apps/frontend/src/components/forms/resetPasswordModal.tsx @@ -11,6 +11,7 @@ import { } from '@chakra-ui/react'; import { resetPassword, confirmResetPassword } from 'aws-amplify/auth'; import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../../hooks/alert'; const ResetPasswordModal: React.FC = () => { const [email, setEmail] = useState(''); @@ -18,7 +19,7 @@ const ResetPasswordModal: React.FC = () => { const [step, setStep] = useState<'reset' | 'new'>('reset'); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); - const [alertMessage, setAlertMessage] = useState(''); + const [alertState, setAlertMessage] = useAlert(); const navigate = useNavigate(); @@ -26,16 +27,16 @@ const ResetPasswordModal: React.FC = () => { try { await resetPassword({ username: email }); setStep('new'); - } catch (error) { - setAlertMessage('Failed to send verification code: ' + error); + } catch { + setAlertMessage('Failed to send verification code'); } }; const handleResendCode = async () => { try { await resetPassword({ username: email }); - } catch (error) { - setAlertMessage('Failed to send verification code: ' + error); + } catch { + setAlertMessage('Failed to send verification code'); } }; @@ -57,8 +58,8 @@ const ResetPasswordModal: React.FC = () => { newPassword: password, }); navigate('/login'); - } catch (error) { - setAlertMessage('Failed to set new password: ' + error); + } catch { + setAlertMessage('Failed to set new password'); } }; @@ -92,8 +93,13 @@ const ResetPasswordModal: React.FC = () => { borderRadius="xl" boxShadow="xl" > - {alertMessage && ( - + {alertState && ( + )} diff --git a/apps/frontend/src/containers/adminDonation.tsx b/apps/frontend/src/containers/adminDonation.tsx index 9c9a761f3..5a513a045 100644 --- a/apps/frontend/src/containers/adminDonation.tsx +++ b/apps/frontend/src/containers/adminDonation.tsx @@ -16,6 +16,7 @@ import DonationDetailsModal from '@components/forms/donationDetailsModal'; import ApiClient from '@api/apiClient'; import { formatDate } from '@utils/utils'; import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../hooks/alert'; const AdminDonation: React.FC = () => { const [donations, setDonations] = useState([]); @@ -29,19 +30,19 @@ const AdminDonation: React.FC = () => { null, ); - const [alertMessage, setAlertMessage] = useState(''); + const [alertState, setAlertMessage] = useAlert(); useEffect(() => { const fetchDonations = async () => { try { const data = await ApiClient.getAllDonations(); setDonations(data); - } catch (error) { - setAlertMessage('Error fetching donations: ' + error); + } catch { + setAlertMessage('Error fetching donations'); } }; fetchDonations(); - }, []); + }, [setAlertMessage]); useEffect(() => { setCurrentPage(1); @@ -102,8 +103,13 @@ const AdminDonation: React.FC = () => { Donation Management - {alertMessage && ( - + {alertState && ( + )} diff --git a/apps/frontend/src/containers/adminOrderManagement.tsx b/apps/frontend/src/containers/adminOrderManagement.tsx index 772861e4e..ebf5a866b 100644 --- a/apps/frontend/src/containers/adminOrderManagement.tsx +++ b/apps/frontend/src/containers/adminOrderManagement.tsx @@ -25,6 +25,7 @@ import ApiClient from '@api/apiClient'; import { OrderStatus, OrderSummary } from '../types/types'; import OrderDetailsModal from '@components/forms/orderDetailsModal'; import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../hooks/alert'; // Extending the OrderSummary type to include assignee color for display type OrderWithColor = OrderSummary & { assigneeColor?: string }; @@ -51,7 +52,7 @@ const AdminOrderManagement: React.FC = () => { }, ); - const [alertMessage, setAlertMessage] = useState(''); + const [alertState, setAlertMessage] = useAlert(); // State to hold filter state per status type FilterState = { @@ -139,13 +140,13 @@ const AdminOrderManagement: React.FC = () => { [OrderStatus.DELIVERED]: 1, }; setCurrentPages(initialPages); - } catch (error) { - setAlertMessage('Error fetching orders: ' + error); + } catch { + setAlertMessage('Error fetching orders'); } }; fetchOrders(); - }, []); + }, [setAlertMessage]); // Helper to reset page for a specific status const resetPageForStatus = (status: OrderStatus) => { @@ -165,8 +166,13 @@ const AdminOrderManagement: React.FC = () => { Order Management - {alertMessage && ( - + {alertState && ( + )} {Object.values(OrderStatus).map((status) => { diff --git a/apps/frontend/src/containers/approvePantries.tsx b/apps/frontend/src/containers/approvePantries.tsx index d2bd59c85..9d06a8bc5 100644 --- a/apps/frontend/src/containers/approvePantries.tsx +++ b/apps/frontend/src/containers/approvePantries.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { Center, Table, @@ -7,25 +8,19 @@ import { NativeSelect, NativeSelectIndicator, } from '@chakra-ui/react'; -import PantryApplicationModal from '@components/forms/pantryApplicationModal'; import ApiClient from '@api/apiClient'; import { Pantry } from 'types/types'; import { formatDate } from '@utils/utils'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../hooks/alert'; const ApprovePantries: React.FC = () => { + const navigate = useNavigate(); const [pendingPantries, setPendingPantries] = useState([]); const [sortedPantries, setSortedPantries] = useState([]); const [sort, setSort] = useState(''); - const [openPantry, setOpenPantry] = useState(null); - - const fetchPantries = async () => { - try { - const data = await ApiClient.getAllPendingPantries(); - setPendingPantries(data); - } catch (err) { - alert(err); - } - }; + const [searchParams, setSearchParams] = useSearchParams(); + const [alertState, setAlertMessage] = useAlert(); const updatePantry = async ( pantryId: number, @@ -34,14 +29,23 @@ const ApprovePantries: React.FC = () => { try { await ApiClient.updatePantry(pantryId, decision); setPendingPantries((prev) => prev.filter((p) => p.pantryId !== pantryId)); - } catch (error) { - alert(`Error ${decision} pantry: ` + error); + } catch { + setAlertMessage(`Error ${decision} pantry`); } }; useEffect(() => { + const fetchPantries = async () => { + try { + const data = await ApiClient.getAllPendingPantries(); + setPendingPantries(data); + } catch { + setAlertMessage('Error fetching pantries'); + } + }; + fetchPantries(); - }, []); + }, [setAlertMessage]); useEffect(() => { const sorted = [...pendingPantries]; @@ -65,8 +69,31 @@ const ApprovePantries: React.FC = () => { setSortedPantries(sorted); }, [sort, pendingPantries]); + useEffect(() => { + const action = searchParams.get('action'); + const name = searchParams.get('name'); + + if (action && name) { + const message = + action === 'approved' + ? `${name} - Application Accepted` + : `${name} - Application Rejected`; + + setAlertMessage(message); + setSearchParams({}); + } + }, [searchParams, setSearchParams, setAlertMessage]); + return (
+ {alertState && ( + + )} { bg="transparent" color="cyan" fontWeight="600" - onClick={() => setOpenPantry(pantry)} + onClick={() => + navigate(`/pantry-application-details/${pantry.pantryId}`) + } > {pantry.pantryName} @@ -117,13 +146,6 @@ const ApprovePantries: React.FC = () => { ))} - {openPantry && ( - setOpenPantry(null)} - /> - )}
diff --git a/apps/frontend/src/containers/formRequests.tsx b/apps/frontend/src/containers/formRequests.tsx index 86253d4c5..32e04377c 100644 --- a/apps/frontend/src/containers/formRequests.tsx +++ b/apps/frontend/src/containers/formRequests.tsx @@ -20,6 +20,7 @@ import RequestDetailsModal from '@components/forms/requestDetailsModal'; import { formatDate } from '@utils/utils'; import ApiClient from '@api/apiClient'; import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../hooks/alert'; const FormRequests: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); @@ -35,7 +36,7 @@ const FormRequests: React.FC = () => { const [openReadOnlyRequest, setOpenReadOnlyRequest] = useState(null); - const [alertMessage, setAlertMessage] = useState(''); + const [alertState, setAlertMessage] = useAlert(); const pageSize = 10; @@ -52,13 +53,13 @@ const FormRequests: React.FC = () => { if (sortedData.length > 0) { setPreviousRequest(sortedData[0]); } - } catch (error) { - setAlertMessage('Error fetching requests: ' + error); + } catch { + setAlertMessage('Error fetching requests'); } } else { setAlertMessage('No pantry associated with this account.'); } - }, []); + }, [setAlertMessage]); useEffect(() => { fetchRequests(); @@ -74,8 +75,13 @@ const FormRequests: React.FC = () => { Food Request Management - {alertMessage && ( - + {alertState && ( + )} + )} +
+
+
+ ); +}; + +const PantryApplicationDetails: React.FC = () => { + const { applicationId } = useParams<{ applicationId: string }>(); + const navigate = useNavigate(); + const [application, setApplication] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<{ + type: 'network' | 'not_found' | 'invalid' | null; + message: string; + }>({ + type: null, + message: '', + }); + const [alertState, setAlertMessage] = useAlert(); + const [showApproveModal, setShowApproveModal] = useState(false); + const [showDenyModal, setShowDenyModal] = useState(false); + + const fieldContentStyles = { + textStyle: 'p2', + color: 'gray.light', + lineHeight: '1.2', + }; + + const headerStyles = { + textStyle: 'p2', + color: 'neutral.800', + }; + + const sectionHeaderStyles = { + ...headerStyles, + fontWeight: 600, + }; + + const fieldHeaderStyles = { + ...headerStyles, + fontWeight: 500, + mb: 1, + }; + + const fetchApplicationDetails = useCallback(async () => { + try { + setLoading(true); + if (!applicationId) { + setError({ type: 'invalid', message: 'Application ID not provided.' }); + return; + } else if (isNaN(parseInt(applicationId, 10))) { + setError({ + type: 'invalid', + message: 'Application ID is not a number.', + }); + } + const data = await ApiClient.getPantry(parseInt(applicationId, 10)); + if (!data) { + setError({ + type: 'not_found', + message: 'Application not found.', + }); + } + setApplication(data); + } catch (err: unknown) { + if (err instanceof AxiosError) { + if (err.response?.status !== 404 && err.response?.status !== 400) { + setError({ + type: 'network', + message: 'Could not load application details.', + }); + } + } + } finally { + setLoading(false); + } + }, [applicationId]); + + useEffect(() => { + fetchApplicationDetails(); + }, [fetchApplicationDetails]); + + const handleApprove = async () => { + if (application) { + try { + await ApiClient.updatePantry(application.pantryId, 'approve'); + navigate( + '/approve-pantries?action=approved&name=' + application.pantryName, + ); + } catch { + setAlertMessage('Error approving application'); + } + } + }; + + const handleDeny = async () => { + if (application) { + try { + await ApiClient.updatePantry(application.pantryId, 'deny'); + navigate( + '/approve-pantries?action=denied&name=' + application.pantryName, + ); + } catch { + setAlertMessage('Error denying application'); + } + } + }; + + if (loading) { + return ( + } + title="Loading application details..." + isLoading={true} + /> + ); + } + + if (error.message || !application) { + const getIcon = () => { + switch (error.type) { + case 'network': + return ; + case 'not_found': + return ; + default: + return ; + } + }; + + return ( + + ); + } + + const pantryUser = application.pantryUser; + + return ( + + + + Application Details + + + {alertState && ( + + )} + + + + + + Application #{application.pantryId} + + + {application.pantryName} + + + Applied {formatDate(application.dateApplied)} + + + + + + + Point of Contact Information + + + {pantryUser.firstName} {pantryUser.lastName} + + + {formatPhone(pantryUser.phone)} + + {pantryUser.email} + + + + Shipping Address + + + {application.shipmentAddressLine1} + {application.shipmentAddressLine2 && + `, ${application.shipmentAddressLine2}`} + + + {application.shipmentAddressCity},{' '} + {application.shipmentAddressState}{' '} + {application.shipmentAddressZip} + + + {application.shipmentAddressCountry === 'US' + ? 'United States of America' + : application.shipmentAddressCountry ?? ''} + + + + + + + Pantry Details + + + + Name + {application.pantryName} + + + Approximate # of Clients + + {application.allergenClients} + + + + + + + + Food Allergies and Restrictions + + {application.restrictions && + application.restrictions.length > 0 ? ( + + ) : ( + None + )} + + + + + + Accepts Refrigerated Donations? + + + {application.refrigeratedDonation} + + + + + Willing to Reserve Donations for Allergen-Avoidant Individuals + + + {application.reserveFoodForAllergic} + + + + + {application.reservationExplanation && ( + + Justification + + {application.reservationExplanation} + + + )} + + + + + Dedicated section for allergy-friendly items? + + + {application.dedicatedAllergyFriendly + ? 'Yes, we have a dedicated shelf or box' + : 'No'} + + + + + How Often Allergen-Avoidant Clients Visit + + + {application.clientVisitFrequency ?? 'Not specified'} + + + + + + + + Confidence in Identifying the Top 9 Allergens + + + {application.identifyAllergensConfidence ?? 'Not specified'} + + + + + Serves Allergen-Avoidant Children + + + {application.serveAllergicChildren ?? 'Not specified'} + + + + + + Open to SSF Activities + {application.activities && application.activities.length > 0 ? ( + + ) : ( + None + )} + + + + Comments/Concerns + + {application.activitiesComments || '-'} + + + + + + Allergen-free Items in Stock + + {application.itemsInStock} + + + + Client Requests + {application.needMoreOptions} + + + + Subscribed to Newsletter + + {application.newsletterSubscription ? 'Yes' : 'No'} + + + + + + + + setShowApproveModal(false)} + onConfirm={handleApprove} + decision="approve" + pantryName={application.pantryName} + dateApplied={formatDate(application.dateApplied)} + /> + + setShowDenyModal(false)} + onConfirm={handleDeny} + decision="deny" + pantryName={application.pantryName} + dateApplied={formatDate(application.dateApplied)} + /> + + + + + + ); +}; + +export default PantryApplicationDetails; diff --git a/apps/frontend/src/containers/pantryDashboard.tsx b/apps/frontend/src/containers/pantryDashboard.tsx index c085a8157..37aa458dc 100644 --- a/apps/frontend/src/containers/pantryDashboard.tsx +++ b/apps/frontend/src/containers/pantryDashboard.tsx @@ -12,12 +12,12 @@ import { } from '@chakra-ui/react'; import { MenuIcon } from 'lucide-react'; import React, { useEffect, useState } from 'react'; -import { Pantry } from 'types/types'; +import { PantryWithUser } from 'types/types'; import ApiClient from '@api/apiClient'; const PantryDashboard: React.FC = () => { const [pantryId, setPantryId] = useState(null); - const [pantry, setPantry] = useState(null); + const [pantry, setPantry] = useState(null); useEffect(() => { const fetchPantryId = async () => { diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index a89318958..7190d7181 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -16,6 +16,7 @@ import { Pantry } from 'types/types'; import { RefrigeratedDonation } from '../types/pantryEnums'; import { FloatingAlert } from '@components/floatingAlert'; import { useNavigate } from 'react-router-dom'; +import { useAlert } from '../hooks/alert'; const AssignedPantries: React.FC = () => { const navigate = useNavigate(); @@ -26,7 +27,7 @@ const AssignedPantries: React.FC = () => { new Set(), ); const [pantrySearch, setPantrySearch] = useState(''); - const [alertMessage, setAlertMessage] = useState(null); + const [alertState, setAlertMessage] = useAlert(); useEffect(() => { const fetchAssignedPantries = async () => { @@ -50,7 +51,7 @@ const AssignedPantries: React.FC = () => { }; fetchAssignedPantries(); - }, []); + }, [setAlertMessage]); const isRefrigeratorFriendly = (pantry: Pantry): boolean => { return ( @@ -111,8 +112,13 @@ const AssignedPantries: React.FC = () => { return ( - {alertMessage && ( - + {alertState && ( + )} diff --git a/apps/frontend/src/containers/volunteerManagement.tsx b/apps/frontend/src/containers/volunteerManagement.tsx index 43cff158f..a597f83c3 100644 --- a/apps/frontend/src/containers/volunteerManagement.tsx +++ b/apps/frontend/src/containers/volunteerManagement.tsx @@ -17,13 +17,14 @@ import { User } from '../types/types'; import ApiClient from '@api/apiClient'; import NewVolunteerModal from '@components/forms/addNewVolunteerModal'; import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../hooks/alert'; const VolunteerManagement: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); const [volunteers, setVolunteers] = useState([]); const [searchName, setSearchName] = useState(''); - const [alertMessage, setAlertMessage] = useState(''); + const [alertState, setAlertMessage] = useAlert(); const [submitSuccess, setSubmitSuccess] = useState(false); const pageSize = 8; @@ -35,13 +36,13 @@ const VolunteerManagement: React.FC = () => { try { const allVolunteers = await ApiClient.getVolunteers(); setVolunteers(allVolunteers); - } catch (error) { - setAlertMessage('Error fetching volunteers: ' + error); + } catch { + setAlertMessage('Error fetching volunteers'); } }; fetchVolunteers(); - }, [alertMessage]); + }, [setAlertMessage]); useEffect(() => { setCurrentPage(1); @@ -68,9 +69,10 @@ const VolunteerManagement: React.FC = () => { Volunteer Management - {alertMessage && ( + {alertState && ( diff --git a/apps/frontend/src/hooks/alert.ts b/apps/frontend/src/hooks/alert.ts new file mode 100644 index 000000000..0a2c609b3 --- /dev/null +++ b/apps/frontend/src/hooks/alert.ts @@ -0,0 +1,17 @@ +import { useCallback, useRef, useState } from 'react'; + +export interface AlertState { + message: string; + id: number; +} + +export function useAlert(): [AlertState | null, (message: string) => void] { + const [alertState, setAlertState] = useState(null); + const idRef = useRef(0); + + const setAlertMessage = useCallback((message: string) => { + setAlertState({ message, id: idRef.current++ }); + }, []); + + return [alertState, setAlertMessage]; +} diff --git a/apps/frontend/src/theme.ts b/apps/frontend/src/theme.ts index abf16ad8a..0b3200042 100644 --- a/apps/frontend/src/theme.ts +++ b/apps/frontend/src/theme.ts @@ -53,6 +53,13 @@ const textStyles = defineTextStyles({ fontWeight: '400', }, }, + p3: { + value: { + fontFamily: 'inter', + fontSize: '12px', + fontWeight: '400', + }, + }, }); const customConfig = defineConfig({ diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 7f28b9d97..c6bec058a 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -12,19 +12,15 @@ import { ManufacturerAttribute, } from './manufacturerEnums'; -// Note: The API calls as currently written do not -// return a pantry's SSF representative or pantry -// representative, or their IDs, as part of the -// Pantry data export interface Pantry { pantryId: number; pantryName: string; - shippingAddressLine1: string; - shippingAddressLine2?: string; - shippingAddressCity: string; - shippingAddressState: string; - shippingAddressZip: string; - shippingAddressCountry?: string; + shipmentAddressLine1: string; + shipmentAddressLine2?: string; + shipmentAddressCity: string; + shipmentAddressState: string; + shipmentAddressZip: string; + shipmentAddressCountry?: string; mailingAddressLine1: string; mailingAddressLine2?: string; mailingAddressCity: string; @@ -49,7 +45,6 @@ export interface Pantry { secondaryContactLastName?: string; secondaryContactEmail?: string; secondaryContactPhone?: string; - pantryUser?: User; status: ApplicationStatus; dateApplied: string; activities: Activity[]; @@ -59,6 +54,10 @@ export interface Pantry { volunteers?: User[]; } +export interface PantryWithUser extends Pantry { + pantryUser: User; +} + export interface PantryApplicationDto { contactFirstName: string; contactLastName: string; @@ -71,12 +70,12 @@ export interface PantryApplicationDto { secondaryContactEmail?: string; secondaryContactPhone?: string; pantryName: string; - shippingAddressLine1: string; - shippingAddressLine2?: string; - shippingAddressCity: string; - shippingAddressState: string; - shippingAddressZip: string; - shippingAddressCountry?: string; + shipmentAddressLine1: string; + shipmentAddressLine2?: string; + shipmentAddressCity: string; + shipmentAddressState: string; + shipmentAddressZip: string; + shipmentAddressCountry?: string; mailingAddressLine1: string; mailingAddressLine2?: string; mailingAddressCity: string; diff --git a/apps/frontend/src/utils/utils.ts b/apps/frontend/src/utils/utils.ts index 2b7faa213..4e704f80a 100644 --- a/apps/frontend/src/utils/utils.ts +++ b/apps/frontend/src/utils/utils.ts @@ -1,5 +1,14 @@ import { DayOfWeek, RecurrenceEnum, RepeatOnState } from '../types/types'; +export const formatPhone = (phone?: string | null) => { + if (!phone) return null; + const digits = phone.replace(/\D/g, ''); + if (digits.length === 10) { + return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`; + } + return phone; +}; + export const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('en-US');