diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index 95009ac2..76e1f986 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -7,13 +7,13 @@ import PantryPastOrders from '@containers/pantryPastOrders'; import Pantries from '@containers/pantries'; import Orders from '@containers/orders'; import PantryDashboard from '@containers/pantryDashboard'; -import submitFoodRequestFormModal from '@components/forms/requestFormModal'; import { submitDeliveryConfirmationFormModal } from '@components/forms/deliveryConfirmationModal'; import FormRequests from '@containers/formRequests'; import PantryApplication from '@containers/pantryApplication'; import PantryApplicationSubmitted from '@containers/pantryApplicationSubmitted'; 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 DonationManagement from '@containers/donationManagement'; @@ -168,6 +168,14 @@ const router = createBrowserRouter([ ), }, + { + path: '/application-details/:applicationId', + element: ( + + + + ), + }, { path: '/admin-donation', element: ( diff --git a/apps/frontend/src/containers/applicationDetails.tsx b/apps/frontend/src/containers/applicationDetails.tsx new file mode 100644 index 00000000..e9c80811 --- /dev/null +++ b/apps/frontend/src/containers/applicationDetails.tsx @@ -0,0 +1,415 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Center, + Box, + Grid, + GridItem, + Text, + Button, + Heading, + VStack, + HStack, + Spinner, + Badge, + Flex, +} from '@chakra-ui/react'; +import ApiClient from '@api/apiClient'; +import { Pantry } from 'types/types'; + +type PantryWithShipment = Pantry & { + shipmentAddressLine1?: string | null; + shipmentAddressLine2?: string | null; + shipmentAddressCity?: string | null; + shipmentAddressState?: string | null; + shipmentAddressZip?: string | null; + shipmentAddressCountry?: string | null; +}; + +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(null); + + 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; + }; + + const fetchApplicationDetails = useCallback(async () => { + try { + setLoading(true); + setError(null); + if (!applicationId) { + setError('Application ID not provided'); + return; + } + const data = await ApiClient.getPantry(parseInt(applicationId, 10)); + setApplication(data as PantryWithShipment); + } catch (err) { + setError( + 'Error loading application details: ' + + (err instanceof Error ? err.message : String(err)), + ); + } finally { + setLoading(false); + } + }, [applicationId]); + + useEffect(() => { + fetchApplicationDetails(); + }, [fetchApplicationDetails]); + + const handleApprove = async () => { + if (application) { + try { + await ApiClient.updatePantry(application.pantryId, 'approve'); + navigate('/approve-pantries'); + } catch (err) { + alert('Error approving application: ' + err); + } + } + }; + + const handleDeny = async () => { + if (application) { + try { + await ApiClient.updatePantry(application.pantryId, 'deny'); + navigate('/approve-pantries'); + } catch (err) { + alert('Error denying application: ' + err); + } + } + }; + + if (loading) { + return ( +
+ + Loading application details... +
+ ); + } + + if (error) { + return ( +
+ + {error} + + +
+ ); + } + + if (!application) { + return ( +
+ + Application not found + + +
+ ); + } + + const pantryUser = application.pantryUser; + + return ( + + + {/* Page Title */} + + Application Details + + + {/* Main Content Card */} + + + {/* Application Header */} + + + Application #{application.pantryId} + + + {application.pantryName} + + + Applied{' '} + {new Date(application.dateApplied).toLocaleDateString('en-US', { + month: '2-digit', + day: '2-digit', + year: 'numeric', + })} + + + + {/* Point of Contact and Shipping Address */} + + + + 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'} + + + + + + Shipping 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 */} + + + Pantry Details + + + + + Name + + {application.pantryName} + + + + Approximate # of Clients + + {application.allergenClients} + + + + + {/* Food Allergies and Restrictions */} + + + Food Allergies and Restrictions + + + {application.restrictions && + application.restrictions.length > 0 ? ( + application.restrictions.map((restriction, index) => ( + + {restriction} + + )) + ) : ( + 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'} + + + + + + + + Confident in Identifying the Top 9 Allergens + + + {application.identifyAllergensConfidence ?? 'Not specified'} + + + + + Serves Allergen-Avoidant Children + + + {application.serveAllergicChildren ?? 'Not specified'} + + + + + + {/* Open to SSF Activities */} + + + Open to SSF Activities + + + {application.activities && application.activities.length > 0 ? ( + application.activities.map((activity, index) => ( + + {activity} + + )) + ) : ( + None + )} + + + + {/* Comments/Concerns */} + + + Comments/Concerns + + {application.activitiesComments || '-'} + + + {/* Allergen-free Items in Stock */} + + + Allergen-free Items in Stock + + {application.itemsInStock} + + + {/* Client Requests */} + + + Client Requests + + {application.needMoreOptions} + + + {/* Subscribed to Newsletter */} + + + Subscribed to Newsletter + + + {application.newsletterSubscription ? 'Yes' : 'No'} + + + + {/* Action Buttons */} + + + + + + + + + ); +}; + +export default ApplicationDetails; diff --git a/apps/frontend/src/containers/approvePantries.tsx b/apps/frontend/src/containers/approvePantries.tsx index d2bd59c8..e59ff71f 100644 --- a/apps/frontend/src/containers/approvePantries.tsx +++ b/apps/frontend/src/containers/approvePantries.tsx @@ -1,41 +1,39 @@ import React, { useEffect, useState } from 'react'; import { - Center, Table, Button, + Box, + Heading, + VStack, + Checkbox, + Pagination, + ButtonGroup, + IconButton, Link, - 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 { + ArrowDownUp, + ChevronLeft, + ChevronRight, + CircleCheck, + Funnel, +} from 'lucide-react'; const ApprovePantries: React.FC = () => { - const [pendingPantries, setPendingPantries] = useState([]); - const [sortedPantries, setSortedPantries] = useState([]); - const [sort, setSort] = useState(''); - const [openPantry, setOpenPantry] = useState(null); + const [pantries, setPantries] = useState([]); + const [sortAsc, setSortAsc] = useState(true); + const [selectedPantries, setSelectedPantries] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [isFilterOpen, setIsFilterOpen] = useState(false); const fetchPantries = async () => { try { const data = await ApiClient.getAllPendingPantries(); - setPendingPantries(data); - } catch (err) { - alert(err); - } - }; - - const updatePantry = async ( - pantryId: number, - decision: 'approve' | 'deny', - ) => { - try { - await ApiClient.updatePantry(pantryId, decision); - setPendingPantries((prev) => prev.filter((p) => p.pantryId !== pantryId)); + setPantries(data); } catch (error) { - alert(`Error ${decision} pantry: ` + error); + alert('Error fetching unapproved pantries: ' + error); } }; @@ -44,89 +42,303 @@ const ApprovePantries: React.FC = () => { }, []); useEffect(() => { - const sorted = [...pendingPantries]; + setCurrentPage(1); + }, [selectedPantries]); - if (sort === 'name') { - sorted.sort((a, b) => a.pantryName.localeCompare(b.pantryName)); - } else if (sort === 'name-reverse') { - sorted.sort((a, b) => b.pantryName.localeCompare(a.pantryName)); - } else if (sort === 'date-recent') { - sorted.sort( - (a, b) => - new Date(b.dateApplied).getTime() - new Date(a.dateApplied).getTime(), - ); - } else if (sort === 'date-oldest') { - sorted.sort( - (a, b) => - new Date(a.dateApplied).getTime() - new Date(b.dateApplied).getTime(), - ); + const pantryOptions = [ + ...new Set( + pantries + .map((p) => p.pantryName) + .filter((name): name is string => !!name), + ), + ].sort((a, b) => a.localeCompare(b)); + + const handleFilterChange = (pantry: string, checked: boolean) => { + if (checked) { + setSelectedPantries([...selectedPantries, pantry]); + } else { + setSelectedPantries(selectedPantries.filter((p) => p !== pantry)); } + }; + + const filteredPantries = pantries + .filter((p) => { + const matchesFilter = + selectedPantries.length === 0 || + selectedPantries.includes(p.pantryName); + return matchesFilter; + }) + .sort((a, b) => + sortAsc + ? new Date(a.dateApplied).getTime() - new Date(b.dateApplied).getTime() + : new Date(b.dateApplied).getTime() - new Date(a.dateApplied).getTime(), + ); + + const itemsPerPage = 10; + const totalPages = Math.ceil(filteredPantries.length / itemsPerPage); + const paginatedPantries = filteredPantries.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage, + ); - setSortedPantries(sorted); - }, [sort, pendingPantries]); + const tableHeaderStyles = { + borderBottom: '1px solid', + borderColor: 'neutral.100', + color: 'neutral.800', + fontFamily: 'inter', + fontWeight: '600', + fontSize: 'sm', + }; return ( -
- - setSort(e.target.value)} + + + Application Review + + {filteredPantries.length === 0 ? ( + - - - - - - - + + + + + No Applications + + + There are no applications to review at this time + + + ) : ( + + + + - - - {sortedPantries.map((pantry) => ( - - {pantry.pantryId} - - + + + + + + Application # + + + Pantry + + + Date Applied + + - {pantry.pantryName} - - - {formatDate(pantry.dateApplied)} - - - - - - - - ))} - {openPantry && ( - setOpenPantry(null)} - /> + + + + )} - - -
+ + )} + ); };