diff --git a/apps/backend/src/donationItems/donationItems.controller.ts b/apps/backend/src/donationItems/donationItems.controller.ts index 6435dd9ca..0ee59ce09 100644 --- a/apps/backend/src/donationItems/donationItems.controller.ts +++ b/apps/backend/src/donationItems/donationItems.controller.ts @@ -6,7 +6,6 @@ import { Get, Patch, ParseIntPipe, - BadRequestException, } from '@nestjs/common'; import { ApiBody } from '@nestjs/swagger'; import { DonationItemsService } from './donationItems.service'; @@ -19,8 +18,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/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index e075e9857..4adb1c3e0 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -1,7 +1,6 @@ import { Controller, Get, - Post, Patch, Param, ParseIntPipe, diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 25a96a7ba..6d914e95c 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -200,9 +200,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 c844de298..a49dde7b3 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -13,6 +13,7 @@ import PantryApplication from '@containers/pantryApplication'; import ApplicationSubmitted from '@containers/applicationSubmitted'; import { submitPantryApplicationForm } from '@components/forms/pantryApplicationForm'; import ApprovePantries from '@containers/approvePantries'; +import ApplicationDetails from '@containers/applicationDetails'; import VolunteerManagement from '@containers/volunteerManagement'; import FoodManufacturerOrderDashboard from '@containers/foodManufacturerOrderDashboard'; import AdminDonation from '@containers/adminDonation'; @@ -174,6 +175,14 @@ const router = createBrowserRouter([ ), }, + { + path: '/application-details/:applicationId', + element: ( + + + + ), + }, { path: '/admin-donation', element: ( diff --git a/apps/frontend/src/components/floatingAlert.tsx b/apps/frontend/src/components/floatingAlert.tsx index 0257b0b36..5eb71eaa1 100644 --- a/apps/frontend/src/components/floatingAlert.tsx +++ b/apps/frontend/src/components/floatingAlert.tsx @@ -5,12 +5,14 @@ type FloatingAlertProps = { message?: string | null; status?: 'info' | 'error'; timeout?: number; + onClose?: () => void; }; export function FloatingAlert({ message, status, timeout, + onClose, }: FloatingAlertProps) { const [visible, setVisible] = useState(!!message); @@ -26,10 +28,11 @@ export function FloatingAlert({ const timer = setTimeout(() => { setVisible(false); + onClose?.(); }, timeout); return () => clearTimeout(timer); - }, [message, timeout]); + }, [message, timeout, onClose]); if (!message || !visible) return null; diff --git a/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx b/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx new file mode 100644 index 000000000..d847f2e8a --- /dev/null +++ b/apps/frontend/src/components/forms/confirmPantryDecisionModal.tsx @@ -0,0 +1,87 @@ +import { Dialog, Text, Box, Button, CloseButton } from '@chakra-ui/react'; + +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..66b98e4dc 100644 --- a/apps/frontend/src/components/forms/donationDetailsModal.tsx +++ b/apps/frontend/src/components/forms/donationDetailsModal.tsx @@ -39,8 +39,8 @@ const DonationDetailsModal: React.FC = ({ ); setItems(itemsData); - } catch (err) { - setAlertMessage('Error fetching donation details: ' + err); + } catch { + setAlertMessage('Error fetching donation details'); } }; diff --git a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx index 38c59bcb1..076cb6c51 100644 --- a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx +++ b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx @@ -715,7 +715,6 @@ export const submitManufacturerApplicationForm: ActionFunction = async ({ ); } else { alert('Form submission failed; please try again'); - console.log(error); } }, ); diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index 91ad1472f..b960ee561 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -199,7 +199,6 @@ const NewDonationFormModal: React.FC = ({ try { const donationResponse = await ApiClient.postDonation(donation_body); - console.log('Submitted donation'); const donationId = donationResponse?.donationId; if (donationId) { diff --git a/apps/frontend/src/components/forms/orderDetailsModal.tsx b/apps/frontend/src/components/forms/orderDetailsModal.tsx index ce5e22f11..83178d80d 100644 --- a/apps/frontend/src/components/forms/orderDetailsModal.tsx +++ b/apps/frontend/src/components/forms/orderDetailsModal.tsx @@ -29,8 +29,8 @@ const OrderDetailsModal: React.FC = ({ order.orderId, ); setFoodRequest(foodRequestData); - } catch (error) { - setAlertMessage('Error fetching food request details:' + error); + } catch { + setAlertMessage('Error fetching food request details'); } }; diff --git a/apps/frontend/src/components/forms/pantryApplicationModal.tsx b/apps/frontend/src/components/forms/pantryApplicationModal.tsx index 4c973fd67..ede50a2e6 100644 --- a/apps/frontend/src/components/forms/pantryApplicationModal.tsx +++ b/apps/frontend/src/components/forms/pantryApplicationModal.tsx @@ -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 07216c6a3..68f6f0cc0 100644 --- a/apps/frontend/src/components/forms/requestFormModal.tsx +++ b/apps/frontend/src/components/forms/requestFormModal.tsx @@ -70,7 +70,7 @@ const FoodRequestFormModal: React.FC = ({ try { await apiClient.createFoodRequest(foodRequestData); - setAlert({ isError: false, message: 'Request Submitted' }); + setAlert({ isError: false, message: 'Request submitted' }); onClose(); onSuccess(); } catch { @@ -91,7 +91,12 @@ const FoodRequestFormModal: React.FC = ({ )} {alert.message && !alert.isError && ( - + setAlert({ isError: true, message: '' })} + /> )} diff --git a/apps/frontend/src/components/forms/resetPasswordModal.tsx b/apps/frontend/src/components/forms/resetPasswordModal.tsx index 8649a9174..58939ec43 100644 --- a/apps/frontend/src/components/forms/resetPasswordModal.tsx +++ b/apps/frontend/src/components/forms/resetPasswordModal.tsx @@ -26,16 +26,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 +57,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'); } }; diff --git a/apps/frontend/src/containers/adminDonation.tsx b/apps/frontend/src/containers/adminDonation.tsx index 9c9a761f3..ef7851009 100644 --- a/apps/frontend/src/containers/adminDonation.tsx +++ b/apps/frontend/src/containers/adminDonation.tsx @@ -36,8 +36,8 @@ const AdminDonation: React.FC = () => { try { const data = await ApiClient.getAllDonations(); setDonations(data); - } catch (error) { - setAlertMessage('Error fetching donations: ' + error); + } catch { + setAlertMessage('Error fetching donations'); } }; fetchDonations(); diff --git a/apps/frontend/src/containers/applicationDetails.tsx b/apps/frontend/src/containers/applicationDetails.tsx new file mode 100644 index 000000000..a878232cf --- /dev/null +++ b/apps/frontend/src/containers/applicationDetails.tsx @@ -0,0 +1,466 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { + Box, + Grid, + GridItem, + Text, + Button, + Heading, + VStack, + HStack, + Spinner, +} from '@chakra-ui/react'; +import ApiClient from '@api/apiClient'; +import { Pantry } from 'types/types'; +import { formatDate, formatPhone } from '@utils/utils'; +import { TagGroup } from '@components/forms/tagGroup'; +import { FileX, TriangleAlert, WifiOff } from 'lucide-react'; +import { AxiosError } from 'axios'; +import { FloatingAlert } from '@components/floatingAlert'; +import ConfirmPantryDecisionModal from '@components/forms/confirmPantryDecisionModal'; + +interface EmptyStateProps { + icon: React.ReactNode; + title: string; + subtitle?: string; +} + +const EmptyState: React.FC = ({ icon, title, subtitle }) => { + return ( + + + + Application Details + + + + {icon} + + {title} + + {subtitle && ( + + {subtitle} + + )} + + + + + ); +}; + +const ApplicationDetails: React.FC = () => { + const { applicationId } = useParams<{ applicationId: string }>(); + const navigate = useNavigate(); + const [application, setApplication] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState<{ + isNetwork: boolean; + message: string; + }>({ + isNetwork: false, + message: '', + }); + const [alertMessage, setAlertMessage] = useState(''); + 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({ isNetwork: false, message: 'Application ID not provided.' }); + return; + } else if (isNaN(parseInt(applicationId))) { + setError({ + isNetwork: false, + message: 'Application ID is not a number.', + }); + } + const data = await ApiClient.getPantry(parseInt(applicationId, 10)); + setApplication(data); + } catch (err: unknown) { + if (err instanceof AxiosError) { + if (err.response?.status !== 404 && err.response?.status !== 400) { + setError({ + isNetwork: true, + 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&id=' + application.pantryId, + ); + } catch { + setAlertMessage('Error approving application'); + } + } + }; + + const handleDeny = async () => { + if (application) { + try { + await ApiClient.updatePantry(application.pantryId, 'deny'); + navigate('/approve-pantries?action=denied&id=' + application.pantryId); + } catch { + setAlertMessage('Error denying application'); + } + } + }; + + if (loading) { + return ( + } title="Loading application details..." /> + ); + } + + if (error.message) { + return ( + : } + title={error.message} + subtitle={error.isNetwork ? 'Please try again later.' : undefined} + /> + ); + } + + if (!application) { + return ( + } + title="Application not found." + subtitle="Please try again later." + /> + ); + } + + const pantryUser = application.pantryUser; + + return ( + + + + Application Details + + + {alertMessage && ( + setAlertMessage('')} + /> + )} + + + + + + Application #{application.pantryId} + + + {application.pantryName} + + + Applied {formatDate(application.dateApplied)} + + + + + + + Point of Contact Information + + + {pantryUser + ? `${pantryUser.firstName} ${pantryUser.lastName}` + : application.secondaryContactFirstName && + application.secondaryContactLastName + ? `${application.secondaryContactFirstName} ${application.secondaryContactLastName}` + : 'N/A'} + + + {formatPhone( + pantryUser?.phone ?? application.secondaryContactPhone, + ) ?? 'N/A'} + + + {pantryUser?.email ?? + application.secondaryContactEmail ?? + 'N/A'} + + + + + Shipment Address + + + {application.shipmentAddressLine1 ?? 'N/A'}, + + + {application.shipmentAddressCity ?? 'N/A'},{' '} + {application.shipmentAddressState ?? 'N/A'}{' '} + {application.shipmentAddressZip ?? ''} + + + {application.shipmentAddressCountry === 'US' + ? 'United States of America' + : application.shipmentAddressCountry ?? 'N/A'} + + + + + + + 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 ApplicationDetails; diff --git a/apps/frontend/src/containers/approvePantries.tsx b/apps/frontend/src/containers/approvePantries.tsx index d2bd59c85..44125ddd8 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,16 +8,18 @@ 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'; 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 [searchParams, setSearchParams] = useSearchParams(); + const [alertMessage, setAlertMessage] = useState(''); const fetchPantries = async () => { try { @@ -65,8 +68,26 @@ const ApprovePantries: React.FC = () => { setSortedPantries(sorted); }, [sort, pendingPantries]); + useEffect(() => { + const action = searchParams.get('action'); + const id = searchParams.get('id'); + + if (action && id) { + const message = + action === 'approved' + ? `Application for Pantry ${id} approved` + : `Application for Pantry ${id} denied`; + + setAlertMessage(message); + setSearchParams({}); + } + }, [searchParams, setSearchParams]); + return (
+ {alertMessage && ( + + )} { bg="transparent" color="cyan" fontWeight="600" - onClick={() => setOpenPantry(pantry)} + onClick={() => + navigate(`/application-details/${pantry.pantryId}`) + } > {pantry.pantryName} @@ -117,13 +140,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 335b2c4b5..3f484aa32 100644 --- a/apps/frontend/src/containers/formRequests.tsx +++ b/apps/frontend/src/containers/formRequests.tsx @@ -54,8 +54,8 @@ 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.'); diff --git a/apps/frontend/src/containers/loginPage.tsx b/apps/frontend/src/containers/loginPage.tsx index 7e704da34..92078e8c5 100644 --- a/apps/frontend/src/containers/loginPage.tsx +++ b/apps/frontend/src/containers/loginPage.tsx @@ -30,8 +30,8 @@ const LoginPage: React.FC = () => { try { await signIn({ username: email, password }); navigate(from, { replace: true }); - } catch (error) { - setAlertMessage('Login failed: ' + error); + } catch { + setAlertMessage('Login failed'); } }; @@ -61,7 +61,12 @@ const LoginPage: React.FC = () => { justifyContent="center" > {alertMessage && ( - + setAlertMessage('')} + /> )} { + 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');