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)}
-
- updatePantry(pantry.pantryId, 'approve')}
+ Actions
+
+
+
+
+ {paginatedPantries.map((pantry, index) => (
+
- Approve
-
-
-
- updatePantry(pantry.pantryId, 'deny')}
+
+ {pantry.pantryId}
+
+
+ {pantry.pantryName}
+
+
+ {new Date(pantry.dateApplied).toLocaleDateString('en-US', {
+ month: '2-digit',
+ day: '2-digit',
+ year: 'numeric',
+ })}
+
+
+
+ View Details
+
+
+
+ ))}
+
+
+
+ {totalPages > 1 && (
+ setCurrentPage(e.page)}
+ >
+
+
+
+
+
+ (
+
+ {page.value}
+
+ )}
+ />
+
+
- Deny
-
-
-
- ))}
- {openPantry && (
- setOpenPantry(null)}
- />
+
+
+
+
)}
-
-
-
+
+ )}
+
);
};