From 2ac780b22d105c54e04a975405a37801eaa0ac7a Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Thu, 23 Apr 2026 10:09:22 -0400 Subject: [PATCH 1/7] frontend for pantry management --- apps/frontend/src/api/apiClient.ts | 7 + apps/frontend/src/app.tsx | 9 + .../src/containers/adminPantryManagement.tsx | 417 ++++++++++++++++++ apps/frontend/src/containers/homepage.tsx | 7 + apps/frontend/src/types/types.ts | 15 + 5 files changed, 455 insertions(+) create mode 100644 apps/frontend/src/containers/adminPantryManagement.tsx diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index b6d7fb38..f2fb203b 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -38,6 +38,7 @@ import { FoodRequestWithoutRelations, VolunteerOrder, VolunteerAction, + ApprovedPantryResponse, } from 'types/types'; const defaultBaseUrl = @@ -176,6 +177,12 @@ export class ApiClient { ) as Promise; } + public async getApprovedPantries(): Promise { + return this.get(`/api/pantries/approved`) as Promise< + ApprovedPantryResponse[] + >; + } + public async getPantryFromOrder(orderId: number): Promise { return this.axiosInstance .get(`/api/orders/${orderId}/pantry`) diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index 84b03edc..383de339 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -35,6 +35,7 @@ import VolunteerRequestManagement from '@containers/volunteerRequestManagement'; import AdminDonationStats from '@containers/adminDonationStats'; import ProfilePage from '@containers/profilePage'; import VolunteerOrderManagement from '@containers/volunteerOrderManagement'; +import AdminPantryManagement from '@containers/adminPantryManagement'; Amplify.configure(CognitoAuthConfig); @@ -256,6 +257,14 @@ const router = createBrowserRouter([ ), }, + { + path: '/admin-pantry-management', + element: ( + + + + ), + }, ], }, ]); diff --git a/apps/frontend/src/containers/adminPantryManagement.tsx b/apps/frontend/src/containers/adminPantryManagement.tsx new file mode 100644 index 00000000..a50be648 --- /dev/null +++ b/apps/frontend/src/containers/adminPantryManagement.tsx @@ -0,0 +1,417 @@ +import { useEffect, useState } from 'react'; +import { + Table, + Text, + Flex, + Input, + VStack, + Box, + Pagination, + ButtonGroup, + IconButton, + Link, + Button, + Checkbox, + Badge, +} from '@chakra-ui/react'; +import { ChevronRight, ChevronLeft, Funnel, Search } from 'lucide-react'; +import { ApprovedPantryResponse } from '../types/types'; +import ApiClient from '@api/apiClient'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../hooks/alert'; +import { getInitials } from '@utils/utils'; +import { RefrigeratedDonation } from '../types/pantryEnums'; + +const AdminPantryManagement: React.FC = () => { + const [currentPage, setCurrentPage] = useState(1); + const [pantries, setPantries] = useState([]); + const [searchPantry, setSearchPantry] = useState(''); + + const [selectedPantries, setSelectedPantries] = useState([]); + + const [alertState, setAlertMessage] = useAlert(); + const [submitSuccess, setSubmitSuccess] = useState(false); + + const [isFilterOpen, setIsFilterOpen] = useState(false); + + const pageSize = 8; + + const USER_ICON_COLORS = ['#F89E19', '#CC3538', '#2795A5', '#2B4E60']; + + useEffect(() => { + const fetchPantries = async () => { + try { + const allApprovedPantries = await ApiClient.getApprovedPantries(); + setPantries(allApprovedPantries); + } catch { + setAlertMessage('Error fetching pantries'); + } + }; + + fetchPantries(); + }, [setAlertMessage]); + + useEffect(() => { + setCurrentPage(1); + }, [selectedPantries]); + + const pantryOptions = [...new Set(pantries.map((p) => p.pantryName))].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; + }); + + const paginatedPantries = filteredPantries.slice( + (currentPage - 1) * pageSize, + currentPage * pageSize, + ); + + return ( + + + Pantry Management + + {alertState && ( + + )} + + + + + + {isFilterOpen && ( + <> + setIsFilterOpen(false)} + zIndex={10} + /> + + + + setSearchPantry(e.target.value)} + fontSize="sm" + pl="30px" + border="none" + bg="transparent" + _focus={{ + boxShadow: 'none', + border: 'none', + outline: 'none', + }} + /> + + + {pantryOptions + .filter((pantry) => + pantry + .toLowerCase() + .includes(searchPantry.toLowerCase()), + ) + .map((pantry) => ( + + handleFilterChange(pantry, e.checked) + } + color="black" + size="sm" + > + + + {pantry} + + ))} + + + + )} + + + + + + + Pantry + + + Assignee + + + Refridgerator-Friendly + + + Action + + + + + {paginatedPantries?.map((pantry) => ( + + + + {pantry.pantryName} + + + + + {pantry.volunteers && pantry.volunteers.length > 0 ? ( + (() => { + const volunteers = pantry.volunteers; + const maxVisible = 3; + + const hasOverflow = volunteers.length > maxVisible; + const visibleVolunteers = hasOverflow + ? volunteers.slice(0, maxVisible - 1) + : volunteers; + + const remainingCount = + volunteers.length - (maxVisible - 1); + + return ( + <> + {visibleVolunteers.map((volunteer, index) => ( + + {getInitials( + volunteer.firstName, + volunteer.lastName, + )} + + ))} + + {hasOverflow && ( + + +{remainingCount} + + )} + + ); + })() + ) : ( + + No volunteer + + )} + + + + + {pantry.refrigeratedDonation === RefrigeratedDonation.YES + ? 'Refridgerator-Friendly' + : 'Not Refridgerator-Friendly'} + + + + + View Orders + + + + ))} + + + + setCurrentPage(page)} + > + + + + setCurrentPage((prev) => Math.max(prev - 1, 1)) + } + > + + + + + ( + setCurrentPage(page.value)} + > + {page.value} + + )} + /> + + + + setCurrentPage((prev) => + Math.min(prev + 1, Math.ceil(pantries.length / pageSize)), + ) + } + > + + + + + + + + + ); +}; + +export default AdminPantryManagement; diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index 15814c28..b212ce67 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -178,6 +178,13 @@ const Homepage: React.FC = () => { + + + + Pantry Management + + + diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index ebddfb2a..d6cee602 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -371,6 +371,21 @@ export interface ManufacturerApplicationDto { newsletterSubscription?: boolean; } +export interface ApprovedPantryResponse { + pantryId: number; + pantryName: string; + refrigeratedDonation: RefrigeratedDonation; + volunteers: AssignedVolunteer[]; +} + +export interface AssignedVolunteer { + userId: number; + firstName: string; + lastName: string; + email: string; + phone: string; +} + export interface CreateFoodRequestBody { pantryId: number; requestedSize: RequestSize; From 73f1e0d2cf253e6a12ca5d81eed37144e8687d19 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Thu, 23 Apr 2026 17:37:55 -0400 Subject: [PATCH 2/7] assign volunteers modal --- apps/frontend/src/api/apiClient.ts | 8 + .../forms/assignVolunteersModal.tsx | 272 ++++++++++++++++++ .../src/containers/adminPantryManagement.tsx | 41 ++- apps/frontend/src/types/types.ts | 5 + 4 files changed, 317 insertions(+), 9 deletions(-) create mode 100644 apps/frontend/src/components/forms/assignVolunteersModal.tsx diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index f2fb203b..a13f5ebb 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -39,6 +39,7 @@ import { VolunteerOrder, VolunteerAction, ApprovedPantryResponse, + UpdatePantryVolunteersDto, } from 'types/types'; const defaultBaseUrl = @@ -404,6 +405,13 @@ export class ApiClient { }); } + public async updatePantryVolunteers( + pantryId: number, + body: UpdatePantryVolunteersDto, + ): Promise { + await this.axiosInstance.patch(`api/pantries/${pantryId}/volunteers`, body); + } + public async updateFoodManufacturer( manufacturerId: number, decision: 'approve' | 'deny', diff --git a/apps/frontend/src/components/forms/assignVolunteersModal.tsx b/apps/frontend/src/components/forms/assignVolunteersModal.tsx new file mode 100644 index 00000000..6395f969 --- /dev/null +++ b/apps/frontend/src/components/forms/assignVolunteersModal.tsx @@ -0,0 +1,272 @@ +import ApiClient from '@api/apiClient'; +import { + Box, + Button, + Checkbox, + CloseButton, + Dialog, + Flex, + Input, + InputGroup, + Text, + VStack, +} from '@chakra-ui/react'; +import { useAlert } from '../../hooks/alert'; +import { useEffect, useState } from 'react'; +import { ApprovedPantryResponse, Assignments } from 'types/types'; +import { SearchIcon } from 'lucide-react'; +import { getInitials } from '@utils/utils'; +import { FloatingAlert } from '@components/floatingAlert'; + +interface AssignVolunteersModalProps { + pantry: ApprovedPantryResponse; + onSuccess: () => void; + onClose: () => void; + isOpen: boolean; +} + +type VolunteerDisplay = { + userId: number; + firstName: string; + lastName: string; +}; + +const USER_ICON_COLORS = ['#F89E19', '#CC3538', '#2795A5', '#2B4E60']; + +const AssignVolunteersModal: React.FC = ({ + pantry, + onSuccess, + onClose, + isOpen, +}) => { + const [alertState, setAlertMessage] = useAlert(); + + const [assignedVolunteers, setAssignedVolunteers] = useState< + VolunteerDisplay[] + >([]); + const [unassignedVolunteers, setUnassignedVolunteers] = useState< + VolunteerDisplay[] + >([]); + + const [selectedIds, setSelectedIds] = useState>(new Set()); + + const [searchName, setSearchName] = useState(''); + + const handleSearchNameChange = ( + event: React.ChangeEvent, + ) => { + setSearchName(event.target.value); + }; + + useEffect(() => { + const fetchVolunteers = async () => { + try { + const allVolunteers: Assignments[] = await ApiClient.getVolunteers(); + + const assignedIds = new Set(pantry.volunteers.map((v) => v.userId)); + + const normalized: VolunteerDisplay[] = allVolunteers.map((v) => ({ + userId: v.id, + firstName: v.firstName, + lastName: v.lastName, + })); + + const assigned = normalized.filter((v) => assignedIds.has(v.userId)); + + const unassigned = normalized.filter((v) => !assignedIds.has(v.userId)); + + setAssignedVolunteers(assigned); + setUnassignedVolunteers(unassigned); + setSelectedIds(new Set(pantry.volunteers.map((v) => v.userId))); + } catch { + setAlertMessage('Error fetching volunteers'); + } + }; + + fetchVolunteers(); + }, [pantry, setAlertMessage]); + + const allVolunteers = [...assignedVolunteers, ...unassignedVolunteers]; + + const filteredVolunteers = allVolunteers.filter((v) => { + const fullName = `${v.firstName} ${v.lastName}`.toLowerCase(); + return fullName.includes(searchName.toLowerCase()); + }); + + const handleToggle = (userId: number, checked: boolean) => { + setSelectedIds((prev) => { + const next = new Set(prev); + if (checked) next.add(userId); + else next.delete(userId); + return next; + }); + }; + + const handleSave = async () => { + try { + const originalIds = new Set(pantry.volunteers.map((v) => v.userId)); + + const addVolunteerIds = [...selectedIds].filter( + (id) => !originalIds.has(id), + ); + const removeVolunteerIds = [...originalIds].filter( + (id) => !selectedIds.has(id), + ); + + if (addVolunteerIds.length > 0 || removeVolunteerIds.length > 0) { + await ApiClient.updatePantryVolunteers(pantry.pantryId, { + addVolunteerIds, + removeVolunteerIds, + }); + } + + onSuccess(); + onClose(); + } catch { + setAlertMessage('Error saving volunteer assignments'); + } + }; + + return ( + { + if (!e.open) onClose(); + }} + closeOnInteractOutside + > + {alertState && ( + + )} + + + + + + + + + + Assign Volunteers + + + + + + {pantry.pantryName} + + + + } + > + + + + + {filteredVolunteers.map((volunteer) => ( + + + + {getInitials( + volunteer.firstName, + volunteer.lastName, + )} + + + + {volunteer.firstName} {volunteer.lastName} + + + + + handleToggle(volunteer.userId, e.checked) + } + size="md" + > + + + + + ))} + + {filteredVolunteers.length === 0 && ( + + No volunteers found + + )} + + + + + + + + + + + + ); +}; + +export default AssignVolunteersModal; diff --git a/apps/frontend/src/containers/adminPantryManagement.tsx b/apps/frontend/src/containers/adminPantryManagement.tsx index a50be648..acff7fe0 100644 --- a/apps/frontend/src/containers/adminPantryManagement.tsx +++ b/apps/frontend/src/containers/adminPantryManagement.tsx @@ -21,6 +21,7 @@ import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; import { getInitials } from '@utils/utils'; import { RefrigeratedDonation } from '../types/pantryEnums'; +import AssignVolunteersModal from '@components/forms/assignVolunteersModal'; const AdminPantryManagement: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); @@ -34,23 +35,36 @@ const AdminPantryManagement: React.FC = () => { const [isFilterOpen, setIsFilterOpen] = useState(false); + const [ + selectedPantryToAssignVolunteers, + setSelectedPantryToAssignVolunteers, + ] = useState(null); + const pageSize = 8; const USER_ICON_COLORS = ['#F89E19', '#CC3538', '#2795A5', '#2B4E60']; - useEffect(() => { - const fetchPantries = async () => { - try { - const allApprovedPantries = await ApiClient.getApprovedPantries(); - setPantries(allApprovedPantries); - } catch { - setAlertMessage('Error fetching pantries'); - } - }; + const fetchPantries = async () => { + try { + const allApprovedPantries = await ApiClient.getApprovedPantries(); + setPantries(allApprovedPantries); + } catch { + setSubmitSuccess(false); + + setAlertMessage('Error fetching pantries'); + } + }; + useEffect(() => { fetchPantries(); }, [setAlertMessage]); + const handleAssignVolunteersSuccess = () => { + setSubmitSuccess(true); + setAlertMessage('Successfully assigned volunteers'); + fetchPantries(); + }; + useEffect(() => { setCurrentPage(1); }, [selectedPantries]); @@ -247,6 +261,7 @@ const AdminPantryManagement: React.FC = () => { color="black" variant="underline" textDecorationColor="neutral.700" + onClick={() => setSelectedPantryToAssignVolunteers(pantry)} > {pantry.pantryName} @@ -358,6 +373,14 @@ const AdminPantryManagement: React.FC = () => { ))} + {selectedPantryToAssignVolunteers && ( + setSelectedPantryToAssignVolunteers(null)} + onSuccess={handleAssignVolunteersSuccess} + isOpen={true} + /> + )} diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index d6cee602..467b1af7 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -59,6 +59,11 @@ export interface ConfirmDeliveryDto { feedback?: string; } +export interface UpdatePantryVolunteersDto { + addVolunteerIds?: number[]; + removeVolunteerIds?: number[]; +} + export interface PantryWithUser extends Pantry { pantryUser: User; } From 1da1fcca04ac36ce3b809088a4d5cc11af56b3de Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Thu, 23 Apr 2026 18:02:33 -0400 Subject: [PATCH 3/7] format issues --- apps/frontend/src/api/apiClient.ts | 2 +- apps/frontend/src/app.tsx | 3 ++- apps/frontend/src/containers/homepage.tsx | 5 +++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index d65a0fcf..c64beb61 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -164,7 +164,7 @@ export class ApiClient { } public async getApprovedPantries(): Promise { - return this.get(`/api/pantries/approved`) as Promise< + return this.axiosInstance.get(`/api/pantries/approved`) as Promise< ApprovedPantryResponse[] >; } diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index 3917bbf8..f02404c6 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -37,6 +37,7 @@ import ProfilePage from '@containers/profilePage'; import VolunteerOrderManagement from '@containers/volunteerOrderManagement'; import AdminPantryManagement from '@containers/adminPantryManagement'; import AdminRequestManagement from '@containers/adminRequestManagement'; +import AdminDashboard from '@containers/testAdminDashboard'; Amplify.configure(CognitoAuthConfig); @@ -206,7 +207,7 @@ const router = createBrowserRouter([ path: '/test-admin-dashboard', element: ( - + ), }, diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index 65216be1..d31c723c 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -182,6 +182,11 @@ const Homepage: React.FC = () => { Pantry Management + + + + + Dashboard From f2a01bb998854e41f70c900e82bae25dad3b4243 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Thu, 23 Apr 2026 18:04:24 -0400 Subject: [PATCH 4/7] fetch pantries bug fix --- apps/frontend/src/api/apiClient.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index c64beb61..cdb3cacb 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -164,9 +164,9 @@ export class ApiClient { } public async getApprovedPantries(): Promise { - return this.axiosInstance.get(`/api/pantries/approved`) as Promise< - ApprovedPantryResponse[] - >; + return this.axiosInstance + .get(`/api/pantries/approved`) + .then((response) => response.data); } public async getPantryFromOrder(orderId: number): Promise { From f283a84e094ffbcd5d6b589385512dd9e9d0fc81 Mon Sep 17 00:00:00 2001 From: Justin Wang Date: Fri, 24 Apr 2026 11:33:36 -0400 Subject: [PATCH 5/7] comments --- apps/frontend/src/api/apiClient.ts | 5 +- .../forms/assignVolunteersModal.tsx | 47 +++++++----- .../src/containers/adminPantryManagement.tsx | 72 +++++++++---------- 3 files changed, 70 insertions(+), 54 deletions(-) diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index cdb3cacb..532ae75e 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -408,7 +408,10 @@ export class ApiClient { pantryId: number, body: UpdatePantryVolunteersDto, ): Promise { - await this.axiosInstance.patch(`api/pantries/${pantryId}/volunteers`, body); + await this.axiosInstance.patch( + `/api/pantries/${pantryId}/volunteers`, + body, + ); } public async updateFoodManufacturer( diff --git a/apps/frontend/src/components/forms/assignVolunteersModal.tsx b/apps/frontend/src/components/forms/assignVolunteersModal.tsx index 6395f969..893a66d3 100644 --- a/apps/frontend/src/components/forms/assignVolunteersModal.tsx +++ b/apps/frontend/src/components/forms/assignVolunteersModal.tsx @@ -31,7 +31,7 @@ type VolunteerDisplay = { lastName: string; }; -const USER_ICON_COLORS = ['#F89E19', '#CC3538', '#2795A5', '#2B4E60']; +const USER_ICON_COLORS = ['yellow.core', 'red', 'teal.ssf', 'blue.core']; const AssignVolunteersModal: React.FC = ({ pantry, @@ -157,20 +157,24 @@ const AssignVolunteersModal: React.FC = ({ fontFamily="inter" fontWeight={600} color="black" + mt={3} > Assign Volunteers - + {pantry.pantryName} + + + } + px={3} > = ({ key={volunteer.userId} align="center" justify="space-between" - py={3} borderBottom="1px solid" borderColor="neutral.100" > - + = ({ - - handleToggle(volunteer.userId, e.checked) - } - size="md" + - - - + + handleToggle(volunteer.userId, e.checked) + } + size="md" + > + + + + ))} @@ -254,6 +266,7 @@ const AssignVolunteersModal: React.FC = ({