From 0628a9875ecebfeec67d36531287b958629a6d4c Mon Sep 17 00:00:00 2001 From: bhuvanh66 Date: Tue, 2 Dec 2025 19:00:36 -0500 Subject: [PATCH 01/12] initial frontend assigned pantries implementation --- apps/frontend/src/app.tsx | 5 + .../containers/volunteerAssignedPantries.tsx | 441 ++++++++++++++++++ .../src/types/volunteerAssignments.ts | 18 + 3 files changed, 464 insertions(+) create mode 100644 apps/frontend/src/containers/volunteerAssignedPantries.tsx create mode 100644 apps/frontend/src/types/volunteerAssignments.ts diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index 89e0fccfe..625bd25d4 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -22,6 +22,7 @@ import DonationManagement from '@containers/donationManagement'; import AdminDonation from '@containers/adminDonation'; import { pantryIdLoader } from '@loaders/pantryIdLoader'; import Homepage from '@containers/homepage'; +import AssignedPantries from '@containers/volunteerAssignedPantries' const router = createBrowserRouter([ { @@ -96,6 +97,10 @@ const router = createBrowserRouter([ path: '/volunteer-management', element: , }, + { + path: '/volunteer-assigned-pantries', + element: , + }, ], }, ]); diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx new file mode 100644 index 000000000..665949aa9 --- /dev/null +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -0,0 +1,441 @@ +import React, { useState, useEffect } from 'react'; +import { Funnel } from 'lucide-react'; +import { + Box, + Button, + Table, + Heading, + VStack, + Checkbox, + Text, +} from '@chakra-ui/react'; +import ApiClient from '@api/apiClient'; +import { Pantry } from 'types/types'; +import { RefrigeratedDonation } from '../types/pantryEnums'; +import { Assignments } from 'types/volunteerAssignments'; + +const AssignedPantries: React.FC = () => { + const [assignments, setAssignments] = useState([]); + const [filteredAssignments, setFilteredAssignments] = useState([]); + const [pantryDetails, setPantryDetails] = useState>(new Map()); + const [isFilterOpen, setIsFilterOpen] = useState(false); + const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = useState(null); + const [selectedPantry, setSelectedPantry] = useState(null); + + useEffect(() => { + const fetchAssignments = async () => { + try { + + const data = await ApiClient.getAllAssignments() as Assignments[]; + setAssignments(data); + setFilteredAssignments(data); + + const detailsMap = new Map(); + await Promise.all( + data + .filter(assignment => assignment.pantry) + .map(async (assignment) => { + try { + const pantry = await ApiClient.getPantry(assignment.pantry!.pantryId); + detailsMap.set(assignment.pantry!.pantryId, pantry); + } catch (error) { + console.error(`Error fetching pantry ${assignment.pantry!.pantryId}:`, error); + } + }) + ); + setPantryDetails(detailsMap); + } catch (error) { + console.error('Error fetching assignments:', error); + alert('Error fetching assigned pantries: ' + error); + } + }; + + fetchAssignments(); + }, []); + + useEffect(() => { + + let filtered = [...assignments]; + + if (filterRefrigeratorFriendly !== null) { + filtered = filtered.filter(assignment => { + if (!assignment.pantry) return false; + const pantry = pantryDetails.get(assignment.pantry.pantryId); + if (!pantry) return true; + const isRefrigeratorFriendlyValue = + pantry.refrigeratedDonation === RefrigeratedDonation.YES || + pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES; + return isRefrigeratorFriendlyValue === filterRefrigeratorFriendly; + }); + } + + setFilteredAssignments(filtered); + }, [filterRefrigeratorFriendly, assignments, pantryDetails]); + + const handlePantryClick = async (pantryId: number) => { + // TODO: navigate to pantr details page + try { + const fullPantryDetails = await ApiClient.getPantry(pantryId); + setSelectedPantry(fullPantryDetails); + } catch (error) { + console.error('Error fetching pantry details:', error); + alert('Error fetching pantry details: ' + error); + } + }; + + const handleViewOrders = (pantryId: number) => { + // TODO: Redirect to Order Management page when it's created + console.log('View orders for pantry:', pantryId); + }; + + const isRefrigeratorFriendly = (pantryId: number): boolean => { + const pantry = pantryDetails.get(pantryId); + if (!pantry) return false; + return pantry.refrigeratedDonation === RefrigeratedDonation.YES || + pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES; + }; + + const getRefrigeratorFriendlyText = (pantryId: number): string => { + const pantry = pantryDetails.get(pantryId); + if (!pantry) return 'Loading...'; + if (pantry.refrigeratedDonation === RefrigeratedDonation.YES || + pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES) { + return 'Refrigerator-Friendly'; + } + return 'Not Refrigerator-Friendly'; + }; + + const tableHeaderStyles = { + borderBottom: '1px solid', + borderColor: 'neutral.100', + color: 'neutral.800', + fontFamily: 'inter', + fontWeight: '600', + fontSize: 'sm', + }; + + return ( + + + Assigned Pantries + + + {/* Filter Button */} + + + + + {isFilterOpen && ( + <> + setIsFilterOpen(false)} + zIndex={10} + /> + + + + + setFilterRefrigeratorFriendly(e.checked ? true : null) + } + color="black" + size="sm" + > + + + + setFilterRefrigeratorFriendly(filterRefrigeratorFriendly === true ? null : true)}> + Refrigerator-Friendly Only + + + + + + setFilterRefrigeratorFriendly(e.checked ? false : null) + } + color="black" + size="sm" + > + + + + setFilterRefrigeratorFriendly(filterRefrigeratorFriendly === false ? null : false)}> + Not Refrigerator-Friendly Only + + + + + + )} + + + + {/* Pantries Table */} + + + + + Pantry + + + Refrigerator-Friendly + + + Action + + + + + {filteredAssignments.map((assignment) => { + + if (!assignment.pantry) return null; + + return ( + + + + + + + {assignment.pantry ? getRefrigeratorFriendlyText(assignment.pantry.pantryId) : 'N/A'} + + + + + + + ); + })} + + + + {/* Temporary: Show selected pantry details (will be a separate page later) */} + {selectedPantry && ( + + + Pantry Information + + + Note: This is a temporary view. A dedicated pantry details page will be created later. + + + + Pantry Name: + {selectedPantry.pantryName} + + + + Address: + + {selectedPantry.addressLine1}
+ {selectedPantry.addressLine2 && <>{selectedPantry.addressLine2}
} + {selectedPantry.addressCity}, {selectedPantry.addressState} {selectedPantry.addressZip} + {selectedPantry.addressCountry && <>
{selectedPantry.addressCountry}} +
+
+ + + Status: + {selectedPantry.status} + + + + Date Applied: + {new Date(selectedPantry.dateApplied).toLocaleDateString()} + + + + Approximately how many allergen-avoidant clients does your pantry serve? + {selectedPantry.allergenClients} + + + {selectedPantry.restrictions && selectedPantry.restrictions.length > 0 && ( + + Which food allergies or other medical dietary restrictions do clients at your pantry report? + {selectedPantry.restrictions.join(', ')} + + )} + + + Would you be able to accept refrigerated/frozen donations from us? + {selectedPantry.refrigeratedDonation} + + + + Are you willing to reserve our food shipments for allergen-avoidant individuals? + {selectedPantry.reserveFoodForAllergic} + + + {selectedPantry.reservationExplanation && ( + + Reservation Explanation: + {selectedPantry.reservationExplanation} + + )} + + + Do you have a dedicated shelf or section of your pantry for allergy-friendly items? + + {selectedPantry.dedicatedAllergyFriendly + ? 'Yes, we have a dedicated shelf or box' + : 'No, we keep allergy-friendly items throughout the pantry, depending on the type of item'} + + + + {selectedPantry.clientVisitFrequency && ( + + How often do allergen-avoidant clients visit your food pantry? + {selectedPantry.clientVisitFrequency} + + )} + + {selectedPantry.identifyAllergensConfidence && ( + + Are you confident in identifying the top 9 allergens in an ingredient list? + {selectedPantry.identifyAllergensConfidence} + + )} + + {selectedPantry.serveAllergicChildren && ( + + Do you serve allergen-avoidant or food-allergic children at your pantry? + {selectedPantry.serveAllergicChildren} + + )} + + {selectedPantry.activities && selectedPantry.activities.length > 0 && ( + + What activities are you open to doing with SSF? + {selectedPantry.activities.join(', ')} + + )} + + {selectedPantry.activitiesComments && ( + + Activities Comments: + {selectedPantry.activitiesComments} + + )} + + + What types of allergen-free items, if any, do you currently have in stock? + {selectedPantry.itemsInStock} + + + + Do allergen-avoidant clients at your pantry ever request a greater variety of items or not have enough options? + {selectedPantry.needMoreOptions} + + + + Would you like to subscribe to our quarterly newsletter? + {selectedPantry.newsletterSubscription ? 'Yes' : 'No'} + +
+ +
+ )} +
+ ); +}; + +export default AssignedPantries; \ No newline at end of file diff --git a/apps/frontend/src/types/volunteerAssignments.ts b/apps/frontend/src/types/volunteerAssignments.ts new file mode 100644 index 000000000..73d43f4fa --- /dev/null +++ b/apps/frontend/src/types/volunteerAssignments.ts @@ -0,0 +1,18 @@ +export interface LimitedPantryInfo { + pantryId: number; + pantryName: string; +} + +export interface Assignments { + assignmentId: number; + volunteer: { + id: number; + firstName: string; + lastName: string; + email: string; + phone: string; + role: string; + }; + pantry: LimitedPantryInfo | null; +} + From 8fd4520c694fe895714e1d059909ce71cda8e4f7 Mon Sep 17 00:00:00 2001 From: bhuvanh66 Date: Wed, 3 Dec 2025 00:18:31 -0500 Subject: [PATCH 02/12] cleanup and added page link to homepage --- apps/frontend/src/containers/homepage.tsx | 5 + .../containers/volunteerAssignedPantries.tsx | 154 +----------------- 2 files changed, 6 insertions(+), 153 deletions(-) diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index a753762b9..c39cca69d 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -133,6 +133,11 @@ const Homepage: React.FC = () => { Pantry Overview + + + Volunteer Assigned Pantries + +
diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index 665949aa9..28a6f1f5b 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -20,7 +20,6 @@ const AssignedPantries: React.FC = () => { const [pantryDetails, setPantryDetails] = useState>(new Map()); const [isFilterOpen, setIsFilterOpen] = useState(false); const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = useState(null); - const [selectedPantry, setSelectedPantry] = useState(null); useEffect(() => { const fetchAssignments = async () => { @@ -72,16 +71,6 @@ const AssignedPantries: React.FC = () => { setFilteredAssignments(filtered); }, [filterRefrigeratorFriendly, assignments, pantryDetails]); - const handlePantryClick = async (pantryId: number) => { - // TODO: navigate to pantr details page - try { - const fullPantryDetails = await ApiClient.getPantry(pantryId); - setSelectedPantry(fullPantryDetails); - } catch (error) { - console.error('Error fetching pantry details:', error); - alert('Error fetching pantry details: ' + error); - } - }; const handleViewOrders = (pantryId: number) => { // TODO: Redirect to Order Management page when it's created @@ -252,7 +241,7 @@ const AssignedPantries: React.FC = () => { - - )} ); }; From d950052069fcd15ae600dbbe056738314a71c359 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:43:36 -0500 Subject: [PATCH 03/12] fix endpoint --- apps/frontend/src/api/apiClient.ts | 8 + apps/frontend/src/app.tsx | 1 + apps/frontend/src/containers/homepage.tsx | 4 +- .../containers/volunteerAssignedPantries.tsx | 226 ++++++++++-------- .../src/types/volunteerAssignments.ts | 16 +- 5 files changed, 136 insertions(+), 119 deletions(-) diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 5fd8b6505..32d6b4fb2 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -301,6 +301,14 @@ export class ApiClient { const data = await this.get('/api/pantries/my-id'); return data as number; } + + public async getAllVolunteers(): Promise< + (Omit & { pantryIds: number[] })[] + > { + return this.get('/api/volunteers/') as Promise< + (Omit & { pantryIds: number[] })[] + >; + } } export default new ApiClient(); diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index e516a9972..c59d1f9b6 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -29,6 +29,7 @@ import Unauthorized from '@containers/unauthorized'; import { Authenticator } from '@aws-amplify/ui-react'; import FoodManufacturerApplication from '@containers/foodManufacturerApplication'; import { submitManufacturerApplicationForm } from '@components/forms/manufacturerApplicationForm'; +import AssignedPantries from '@containers/volunteerAssignedPantries'; Amplify.configure(CognitoAuthConfig); diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index 0d3667eb9..7efdea3d2 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -145,7 +145,9 @@ const Homepage: React.FC = () => { - Volunteer Assigned Pantries + + Volunteer Assigned Pantries + diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index 28a6f1f5b..c556c7e62 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo } from 'react'; import { Funnel } from 'lucide-react'; import { Box, @@ -16,31 +16,31 @@ import { Assignments } from 'types/volunteerAssignments'; const AssignedPantries: React.FC = () => { const [assignments, setAssignments] = useState([]); - const [filteredAssignments, setFilteredAssignments] = useState([]); - const [pantryDetails, setPantryDetails] = useState>(new Map()); + const [pantryDetails, setPantryDetails] = useState>( + new Map(), + ); const [isFilterOpen, setIsFilterOpen] = useState(false); - const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = useState(null); + const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = useState< + boolean | null + >(null); useEffect(() => { const fetchAssignments = async () => { try { - - const data = await ApiClient.getAllAssignments() as Assignments[]; + const data = (await ApiClient.getAllVolunteers()) as Assignments[]; setAssignments(data); - setFilteredAssignments(data); - + const detailsMap = new Map(); + const allPantryIds = [...new Set(data.flatMap((a) => a.pantryIds))]; await Promise.all( - data - .filter(assignment => assignment.pantry) - .map(async (assignment) => { - try { - const pantry = await ApiClient.getPantry(assignment.pantry!.pantryId); - detailsMap.set(assignment.pantry!.pantryId, pantry); - } catch (error) { - console.error(`Error fetching pantry ${assignment.pantry!.pantryId}:`, error); - } - }) + allPantryIds.map(async (id) => { + try { + const pantry = await ApiClient.getPantry(id); + detailsMap.set(id, pantry); + } catch (error) { + console.error(`Error fetching pantry ${id}:`, error); + } + }), ); setPantryDetails(detailsMap); } catch (error) { @@ -48,50 +48,47 @@ const AssignedPantries: React.FC = () => { alert('Error fetching assigned pantries: ' + error); } }; - + fetchAssignments(); }, []); - useEffect(() => { + const filteredAssignments = useMemo(() => { + if (filterRefrigeratorFriendly === null) return assignments; - let filtered = [...assignments]; - - if (filterRefrigeratorFriendly !== null) { - filtered = filtered.filter(assignment => { - if (!assignment.pantry) return false; - const pantry = pantryDetails.get(assignment.pantry.pantryId); - if (!pantry) return true; - const isRefrigeratorFriendlyValue = - pantry.refrigeratedDonation === RefrigeratedDonation.YES || - pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES; - return isRefrigeratorFriendlyValue === filterRefrigeratorFriendly; - }); - } - - setFilteredAssignments(filtered); + return assignments + .map((a) => ({ + ...a, + pantryIds: a.pantryIds.filter((id) => { + const pantry = pantryDetails.get(id); + if (!pantry) return false; + const friendly = + pantry.refrigeratedDonation === RefrigeratedDonation.YES || + pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES; + return friendly === filterRefrigeratorFriendly; + }), + })) + .filter((a) => a.pantryIds.length > 0); }, [filterRefrigeratorFriendly, assignments, pantryDetails]); - const handleViewOrders = (pantryId: number) => { - // TODO: Redirect to Order Management page when it's created console.log('View orders for pantry:', pantryId); }; const isRefrigeratorFriendly = (pantryId: number): boolean => { const pantry = pantryDetails.get(pantryId); if (!pantry) return false; - return pantry.refrigeratedDonation === RefrigeratedDonation.YES || - pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES; + return ( + pantry.refrigeratedDonation === RefrigeratedDonation.YES || + pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES + ); }; const getRefrigeratorFriendlyText = (pantryId: number): string => { const pantry = pantryDetails.get(pantryId); if (!pantry) return 'Loading...'; - if (pantry.refrigeratedDonation === RefrigeratedDonation.YES || - pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES) { - return 'Refrigerator-Friendly'; - } - return 'Not Refrigerator-Friendly'; + return isRefrigeratorFriendly(pantryId) + ? 'Refrigerator-Friendly' + : 'Not Refrigerator-Friendly'; }; const tableHeaderStyles = { @@ -108,7 +105,7 @@ const AssignedPantries: React.FC = () => { Assigned Pantries - + {/* Filter Button */} @@ -156,7 +153,7 @@ const AssignedPantries: React.FC = () => { + onCheckedChange={(e: { checked: boolean }) => setFilterRefrigeratorFriendly(e.checked ? true : null) } color="black" @@ -165,15 +162,23 @@ const AssignedPantries: React.FC = () => { - setFilterRefrigeratorFriendly(filterRefrigeratorFriendly === true ? null : true)}> + + setFilterRefrigeratorFriendly( + filterRefrigeratorFriendly === true ? null : true, + ) + } + > Refrigerator-Friendly Only - + + onCheckedChange={(e: { checked: boolean }) => setFilterRefrigeratorFriendly(e.checked ? false : null) } color="black" @@ -182,7 +187,15 @@ const AssignedPantries: React.FC = () => { - setFilterRefrigeratorFriendly(filterRefrigeratorFriendly === false ? null : false)}> + + setFilterRefrigeratorFriendly( + filterRefrigeratorFriendly === false ? null : false, + ) + } + > Not Refrigerator-Friendly Only @@ -223,67 +236,70 @@ const AssignedPantries: React.FC = () => { - {filteredAssignments.map((assignment) => { - - if (!assignment.pantry) return null; - - return ( - - + assignment.pantryIds.map((pantryId) => { + const pantry = pantryDetails.get(pantryId); + return ( + - - - - - {assignment.pantry ? getRefrigeratorFriendlyText(assignment.pantry.pantryId) : 'N/A'} - - - - + + - View Orders - - - - ); - })} + + {getRefrigeratorFriendlyText(pantryId)} + + + + + + + ); + }), + )} ); }; -export default AssignedPantries; \ No newline at end of file +export default AssignedPantries; diff --git a/apps/frontend/src/types/volunteerAssignments.ts b/apps/frontend/src/types/volunteerAssignments.ts index 73d43f4fa..2da58deb4 100644 --- a/apps/frontend/src/types/volunteerAssignments.ts +++ b/apps/frontend/src/types/volunteerAssignments.ts @@ -1,18 +1,8 @@ +import { User } from './types'; + export interface LimitedPantryInfo { pantryId: number; pantryName: string; } -export interface Assignments { - assignmentId: number; - volunteer: { - id: number; - firstName: string; - lastName: string; - email: string; - phone: string; - role: string; - }; - pantry: LimitedPantryInfo | null; -} - +export type Assignments = Omit & { pantryIds: number[] }; From 53af04e20f5f59af223df6c37b0146b3d3ef6301 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:27:09 -0500 Subject: [PATCH 04/12] match figma --- .../containers/volunteerAssignedPantries.tsx | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index c556c7e62..dadacfc22 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -98,6 +98,8 @@ const AssignedPantries: React.FC = () => { fontFamily: 'inter', fontWeight: '600', fontSize: 'sm', + py: 3, + px: 4, }; return ( @@ -207,7 +209,7 @@ const AssignedPantries: React.FC = () => { {/* Pantries Table */} - + { Refrigerator-Friendly Action @@ -239,36 +240,38 @@ const AssignedPantries: React.FC = () => { {filteredAssignments.flatMap((assignment) => assignment.pantryIds.map((pantryId) => { const pantry = pantryDetails.get(pantryId); + const friendly = isRefrigeratorFriendly(pantryId); return ( + {/* Pantry Name */} - + - + + {/* Refrigerator-Friendly Badge */} + { display="inline-block" fontSize="sm" fontFamily="inter" + color="neutral.700" > {getRefrigeratorFriendlyText(pantryId)} - + + {/* Action */} + From 9a8c792a6f21c310d93a0587303516ca750ad2c4 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:24:19 -0500 Subject: [PATCH 05/12] amy comments --- .../src/volunteers/volunteers.controller.ts | 3 + apps/frontend/src/api/apiClient.ts | 9 +- apps/frontend/src/app.tsx | 6 +- apps/frontend/src/containers/homepage.tsx | 22 +- .../containers/volunteerAssignedPantries.tsx | 421 +++++++++--------- apps/frontend/src/types/types.ts | 2 + .../src/types/volunteerAssignments.ts | 8 - 7 files changed, 233 insertions(+), 238 deletions(-) delete mode 100644 apps/frontend/src/types/volunteerAssignments.ts diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index ce6b9d62d..cd7b4feea 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -9,11 +9,14 @@ import { import { User } from '../users/user.entity'; import { Pantry } from '../pantries/pantries.entity'; import { VolunteersService } from './volunteers.service'; +import { Role } from '../users/types'; +import { Roles } from '../auth/roles.decorator'; @Controller('volunteers') export class VolunteersController { constructor(private volunteersService: VolunteersService) {} + @Roles(Role.VOLUNTEER, Role.ADMIN) @Get('/') async getAllVolunteers(): Promise< (Omit & { pantryIds: number[] })[] diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 32d6b4fb2..988548277 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -21,6 +21,7 @@ import { OrderSummary, UserDto, OrderDetails, + Assignments, } from 'types/types'; const defaultBaseUrl = @@ -302,12 +303,8 @@ export class ApiClient { return data as number; } - public async getAllVolunteers(): Promise< - (Omit & { pantryIds: number[] })[] - > { - return this.get('/api/volunteers/') as Promise< - (Omit & { pantryIds: number[] })[] - >; + public async getAllVolunteers(): Promise { + return this.get('/api/volunteers') as Promise; } } diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index c59d1f9b6..81331becb 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -217,7 +217,11 @@ const router = createBrowserRouter([ }, { path: '/volunteer-assigned-pantries', - element: , + element: ( + + + + ), }, ], }, diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index 7efdea3d2..53d3b4068 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -89,6 +89,21 @@ const Homepage: React.FC = () => { + + + Volunteer View + + + + + + Assigned Pantries + + + + + + Admin View @@ -143,13 +158,6 @@ const Homepage: React.FC = () => { Pantry Overview - - - - Volunteer Assigned Pantries - - - diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index dadacfc22..dfb1b17bb 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -6,28 +6,43 @@ import { Table, Heading, VStack, - Checkbox, Text, + RadioGroup, + Spinner, + Center, } from '@chakra-ui/react'; import ApiClient from '@api/apiClient'; import { Pantry } from 'types/types'; import { RefrigeratedDonation } from '../types/pantryEnums'; -import { Assignments } from 'types/volunteerAssignments'; +import { Assignments } from './../types/types'; +import { useNavigate } from 'react-router-dom'; +import { FloatingAlert } from '@components/floatingAlert'; const AssignedPantries: React.FC = () => { + const navigator = useNavigate(); const [assignments, setAssignments] = useState([]); const [pantryDetails, setPantryDetails] = useState>( new Map(), ); const [isFilterOpen, setIsFilterOpen] = useState(false); - const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = useState< - boolean | null - >(null); + const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = + useState('all'); + const [alertMessage, setAlertMessage] = useState(''); + const [isLoading, setIsLoading] = useState(true); + + const isRefrigeratorFriendly = (pantryId: number): boolean => { + const pantry = pantryDetails.get(pantryId); + if (!pantry) return false; + return ( + pantry.refrigeratedDonation === RefrigeratedDonation.YES || + pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES + ); + }; useEffect(() => { const fetchAssignments = async () => { try { - const data = (await ApiClient.getAllVolunteers()) as Assignments[]; + const data = await ApiClient.getAllVolunteers(); setAssignments(data); const detailsMap = new Map(); @@ -45,7 +60,9 @@ const AssignedPantries: React.FC = () => { setPantryDetails(detailsMap); } catch (error) { console.error('Error fetching assignments:', error); - alert('Error fetching assigned pantries: ' + error); + setAlertMessage('Error fetching assigned pantries'); + } finally { + setIsLoading(false); } }; @@ -53,39 +70,22 @@ const AssignedPantries: React.FC = () => { }, []); const filteredAssignments = useMemo(() => { - if (filterRefrigeratorFriendly === null) return assignments; + if (filterRefrigeratorFriendly === 'all') return assignments; + const target = filterRefrigeratorFriendly === 'friendly'; return assignments .map((a) => ({ ...a, pantryIds: a.pantryIds.filter((id) => { const pantry = pantryDetails.get(id); if (!pantry) return false; - const friendly = - pantry.refrigeratedDonation === RefrigeratedDonation.YES || - pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES; - return friendly === filterRefrigeratorFriendly; + return isRefrigeratorFriendly(pantry.pantryId) === target; }), })) .filter((a) => a.pantryIds.length > 0); }, [filterRefrigeratorFriendly, assignments, pantryDetails]); - const handleViewOrders = (pantryId: number) => { - console.log('View orders for pantry:', pantryId); - }; - - const isRefrigeratorFriendly = (pantryId: number): boolean => { - const pantry = pantryDetails.get(pantryId); - if (!pantry) return false; - return ( - pantry.refrigeratedDonation === RefrigeratedDonation.YES || - pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES - ); - }; - const getRefrigeratorFriendlyText = (pantryId: number): string => { - const pantry = pantryDetails.get(pantryId); - if (!pantry) return 'Loading...'; return isRefrigeratorFriendly(pantryId) ? 'Refrigerator-Friendly' : 'Not Refrigerator-Friendly'; @@ -104,209 +104,198 @@ const AssignedPantries: React.FC = () => { return ( - + {alertMessage && ( + + )} + + Assigned Pantries - {/* Filter Button */} - - - - - {isFilterOpen && ( - <> - setIsFilterOpen(false)} - zIndex={10} - /> - + + + ) : ( + <> + {/* Filter Button */} + + + - - - setFilterRefrigeratorFriendly(e.checked ? false : null) - } - color="black" - size="sm" - > - - - - - setFilterRefrigeratorFriendly( - filterRefrigeratorFriendly === false ? null : false, - ) + {isFilterOpen && ( + <> + setIsFilterOpen(false)} + zIndex={10} + /> + + + setFilterRefrigeratorFriendly(e.value) } > - Not Refrigerator-Friendly Only - + + + + + + Show All + + + + + + + Refrigerator-Friendly Only + + + + + + + Not Refrigerator-Friendly Only + + + + - - - - )} - - + + )} + + - {/* Pantries Table */} - - - - - Pantry - - - Refrigerator-Friendly - - - Action - - - - - {filteredAssignments.flatMap((assignment) => - assignment.pantryIds.map((pantryId) => { - const pantry = pantryDetails.get(pantryId); - const friendly = isRefrigeratorFriendly(pantryId); - return ( - + + + - {/* Pantry Name */} - - + + Refrigerator-Friendly + + + Action + + + + + {filteredAssignments.flatMap((assignment) => + assignment.pantryIds.map((pantryId) => { + const pantry = pantryDetails.get(pantryId); + const friendly = isRefrigeratorFriendly(pantryId); + return ( + - {pantry?.pantryName ?? 'Loading...'} - - + {/* Pantry Name */} + + + {pantry?.pantryName} + + - {/* Refrigerator-Friendly Badge */} - - - {getRefrigeratorFriendlyText(pantryId)} - - + {/* Refrigerator-Friendly Badge */} + + + {getRefrigeratorFriendlyText(pantryId)} + + - {/* Action */} - - - - - ); - }), - )} - - + {/* Action */} + + + + + ); + }), + )} + + + + )} ); }; diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 7bceeff4d..0714393c9 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -366,3 +366,5 @@ export type DayOfWeek = | 'Sunday'; export type RepeatOnState = Record; + +export type Assignments = Omit & { pantryIds: number[] }; diff --git a/apps/frontend/src/types/volunteerAssignments.ts b/apps/frontend/src/types/volunteerAssignments.ts deleted file mode 100644 index 2da58deb4..000000000 --- a/apps/frontend/src/types/volunteerAssignments.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { User } from './types'; - -export interface LimitedPantryInfo { - pantryId: number; - pantryName: string; -} - -export type Assignments = Omit & { pantryIds: number[] }; From ddc2590caaae430d254191e12ade0b66c454224d Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:50:30 -0500 Subject: [PATCH 06/12] empty state and table col border --- .../containers/volunteerAssignedPantries.tsx | 207 ++++++++++-------- 1 file changed, 116 insertions(+), 91 deletions(-) diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index dfb1b17bb..2282a71cd 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -10,7 +10,9 @@ import { RadioGroup, Spinner, Center, + Icon, } from '@chakra-ui/react'; +import { CircleCheck } from 'lucide-react'; import ApiClient from '@api/apiClient'; import { Pantry } from 'types/types'; import { RefrigeratedDonation } from '../types/pantryEnums'; @@ -197,103 +199,126 @@ const AssignedPantries: React.FC = () => { + {/* Empty State */} + {filteredAssignments.length === 0 && ( +
+ + + No Assigned Pantries + + + You have no assigned pantries at this time. + +
+ )} + {/* Pantries Table */} - - - - - Pantry - - - Refrigerator-Friendly - - - Action - - - - - {filteredAssignments.flatMap((assignment) => - assignment.pantryIds.map((pantryId) => { - const pantry = pantryDetails.get(pantryId); - const friendly = isRefrigeratorFriendly(pantryId); - return ( - - {/* Pantry Name */} - 0 && ( + + + + + Pantry + + + Refrigerator-Friendly + + + Action + + + + + {filteredAssignments.flatMap((assignment) => + assignment.pantryIds.map((pantryId) => { + const pantry = pantryDetails.get(pantryId); + const friendly = isRefrigeratorFriendly(pantryId); + return ( + - - {pantry?.pantryName} - - + + {pantry?.pantryName} + + - {/* Refrigerator-Friendly Badge */} - - - {getRefrigeratorFriendlyText(pantryId)} - - - - {/* Action */} - - - - - ); - }), - )} - - + + {getRefrigeratorFriendlyText(pantryId)} + + + + {/* Action */} + + + + + ); + }), + )} + + + )} )} From 8246405cc385bf8d3c69755cc6417dd027b96241 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Fri, 6 Mar 2026 10:40:36 -0500 Subject: [PATCH 07/12] sam comments --- .../src/users/users.controller.spec.ts | 11 + apps/backend/src/users/users.controller.ts | 7 + .../src/volunteers/volunteers.controller.ts | 12 +- apps/frontend/src/api/apiClient.ts | 9 +- apps/frontend/src/app.tsx | 32 +- .../containers/volunteerAssignedPantries.tsx | 485 ++++++++---------- 6 files changed, 262 insertions(+), 294 deletions(-) diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 58182c73e..88a26ab51 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -7,6 +7,7 @@ import { userSchemaDto } from './dtos/userSchema.dto'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; +import { AuthenticatedRequest } from '../auth/authenticated-request'; const mockUserService = mock(); @@ -46,6 +47,16 @@ describe('UsersController', () => { expect(controller).toBeDefined(); }); + describe('GET /my-id', () => { + it('should return the current user id', () => { + const req = { user: { id: 1 } } as AuthenticatedRequest; + + const result = controller.getCurrentUserId(req); + + expect(result).toBe(1); + }); + }); + describe('GET /:id', () => { it('should return a user by id', async () => { mockUserService.findOne.mockResolvedValue(mockUser1 as User); diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index 064b688ac..3384d0b39 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -8,16 +8,23 @@ import { Post, BadRequestException, Body, + Req, } from '@nestjs/common'; import { UsersService } from './users.service'; import { User } from './user.entity'; import { Role } from './types'; import { userSchemaDto } from './dtos/userSchema.dto'; +import { AuthenticatedRequest } from '../auth/authenticated-request'; @Controller('users') export class UsersController { constructor(private usersService: UsersService) {} + @Get('/my-id') + getCurrentUserId(@Req() req: AuthenticatedRequest): number { + return req.user.id; + } + @Get('/:id') async getUser(@Param('id', ParseIntPipe) userId: number): Promise { return this.usersService.findOne(userId); diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index cd7b4feea..4e7cc66ed 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -16,7 +16,7 @@ import { Roles } from '../auth/roles.decorator'; export class VolunteersController { constructor(private volunteersService: VolunteersService) {} - @Roles(Role.VOLUNTEER, Role.ADMIN) + @Roles(Role.ADMIN) @Get('/') async getAllVolunteers(): Promise< (Omit & { pantryIds: number[] })[] @@ -24,11 +24,6 @@ export class VolunteersController { return this.volunteersService.getVolunteersAndPantryAssignments(); } - @Get('/:id') - async getVolunteer(@Param('id', ParseIntPipe) userId: number): Promise { - return this.volunteersService.findOne(userId); - } - @Get('/:id/pantries') async getVolunteerPantries( @Param('id', ParseIntPipe) id: number, @@ -36,6 +31,11 @@ export class VolunteersController { return this.volunteersService.getVolunteerPantries(id); } + @Get('/:id') + async getVolunteer(@Param('id', ParseIntPipe) userId: number): Promise { + return this.volunteersService.findOne(userId); + } + @Post('/:id/pantries') async assignPantries( @Param('id', ParseIntPipe) id: number, diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 988548277..ec93c4267 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -180,6 +180,10 @@ export class ApiClient { return this.get('/api/volunteers/') as Promise; } + public async getVolunteerPantries(userId: number): Promise { + return this.get(`/api/volunteers/${userId}/pantries`) as Promise; + } + public async updateUserVolunteerRole( userId: number, body: { role: string }, @@ -303,8 +307,9 @@ export class ApiClient { return data as number; } - public async getAllVolunteers(): Promise { - return this.get('/api/volunteers') as Promise; + public async getMyId(): Promise { + const data = await this.get('/api/users/my-id'); + return data as number; } } diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index 81331becb..7f4e29786 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -167,22 +167,6 @@ const router = createBrowserRouter([ ), }, - { - path: '/approve-pantries', - element: ( - - - - ), - }, - { - path: '/admin-donation', - element: ( - - - - ), - }, { path: '/volunteer-management', element: ( @@ -205,15 +189,19 @@ const router = createBrowserRouter([ }, { path: '/approve-pantries', - element: , + element: ( + + + + ), }, { path: '/admin-donation', - element: , - }, - { - path: '/volunteer-management', - element: , + element: ( + + + + ), }, { path: '/volunteer-assigned-pantries', diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index 2282a71cd..057237d9c 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -1,96 +1,70 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { Funnel } from 'lucide-react'; +import { Funnel, CircleCheck } from 'lucide-react'; import { Box, Button, Table, Heading, VStack, - Text, RadioGroup, - Spinner, - Center, - Icon, + Text, } from '@chakra-ui/react'; -import { CircleCheck } from 'lucide-react'; import ApiClient from '@api/apiClient'; import { Pantry } from 'types/types'; import { RefrigeratedDonation } from '../types/pantryEnums'; -import { Assignments } from './../types/types'; -import { useNavigate } from 'react-router-dom'; import { FloatingAlert } from '@components/floatingAlert'; +import { useNavigate } from 'react-router-dom'; const AssignedPantries: React.FC = () => { - const navigator = useNavigate(); - const [assignments, setAssignments] = useState([]); - const [pantryDetails, setPantryDetails] = useState>( - new Map(), - ); + const navigate = useNavigate(); + const [pantries, setPantries] = useState([]); const [isFilterOpen, setIsFilterOpen] = useState(false); - const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = - useState('all'); - const [alertMessage, setAlertMessage] = useState(''); - const [isLoading, setIsLoading] = useState(true); - - const isRefrigeratorFriendly = (pantryId: number): boolean => { - const pantry = pantryDetails.get(pantryId); - if (!pantry) return false; - return ( - pantry.refrigeratedDonation === RefrigeratedDonation.YES || - pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES - ); - }; + const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = useState< + boolean | null + >(null); + const [alertMessage, setAlertMessage] = useState(null); useEffect(() => { - const fetchAssignments = async () => { + const fetchAssignedPantries = async () => { try { - const data = await ApiClient.getAllVolunteers(); - setAssignments(data); - - const detailsMap = new Map(); - const allPantryIds = [...new Set(data.flatMap((a) => a.pantryIds))]; - await Promise.all( - allPantryIds.map(async (id) => { - try { - const pantry = await ApiClient.getPantry(id); - detailsMap.set(id, pantry); - } catch (error) { - console.error(`Error fetching pantry ${id}:`, error); - } - }), - ); - setPantryDetails(detailsMap); + const userId = await ApiClient.getMyId(); + const data = await ApiClient.getVolunteerPantries(userId); + setPantries(data); } catch (error) { - console.error('Error fetching assignments:', error); + console.error('Error fetching assigned pantries:', error); setAlertMessage('Error fetching assigned pantries'); - } finally { - setIsLoading(false); } }; - fetchAssignments(); + fetchAssignedPantries(); }, []); - const filteredAssignments = useMemo(() => { - if (filterRefrigeratorFriendly === 'all') return assignments; + const isRefrigeratorFriendly = (pantry: Pantry): boolean => { + return ( + pantry.refrigeratedDonation === RefrigeratedDonation.YES || + pantry.refrigeratedDonation === RefrigeratedDonation.SOMETIMES + ); + }; + + const filteredPantries = useMemo(() => { + if (filterRefrigeratorFriendly === null) return pantries; + return pantries.filter( + (pantry) => isRefrigeratorFriendly(pantry) === filterRefrigeratorFriendly, + ); + }, [filterRefrigeratorFriendly, pantries]); - const target = filterRefrigeratorFriendly === 'friendly'; - return assignments - .map((a) => ({ - ...a, - pantryIds: a.pantryIds.filter((id) => { - const pantry = pantryDetails.get(id); - if (!pantry) return false; - return isRefrigeratorFriendly(pantry.pantryId) === target; - }), - })) - .filter((a) => a.pantryIds.length > 0); - }, [filterRefrigeratorFriendly, assignments, pantryDetails]); + // Map radio value string to filter state + const radioValue = + filterRefrigeratorFriendly === true + ? 'yes' + : filterRefrigeratorFriendly === false + ? 'no' + : ''; - const getRefrigeratorFriendlyText = (pantryId: number): string => { - return isRefrigeratorFriendly(pantryId) - ? 'Refrigerator-Friendly' - : 'Not Refrigerator-Friendly'; + const handleRadioChange = (value: string) => { + if (value === 'yes') setFilterRefrigeratorFriendly(true); + else if (value === 'no') setFilterRefrigeratorFriendly(false); + else setFilterRefrigeratorFriendly(null); }; const tableHeaderStyles = { @@ -107,219 +81,202 @@ const AssignedPantries: React.FC = () => { return ( {alertMessage && ( - + )} Assigned Pantries - {isLoading ? ( -
- -
- ) : ( - <> - {/* Filter Button */} - - - + + {isFilterOpen && ( + <> + setIsFilterOpen(false)} + zIndex={10} + /> + - - Filter - + + handleRadioChange(e.value) + } + size="sm" + > + + + + + + Refrigerator-Friendly Only + + + + + + + Not Refrigerator-Friendly Only + + + + + + + )} + + - {isFilterOpen && ( - <> - setIsFilterOpen(false)} - zIndex={10} - /> - + + + No Assigned Pantries + + + You have no assigned pantries at this time. + + + ) : ( + + + + + Pantry + + + Refrigerator-Friendly + + + Action + + + + + {filteredPantries.map((pantry) => ( + + {/* Pantry Name */} + + - - setFilterRefrigeratorFriendly(e.value) - } + {pantry.pantryName} + + + + {/* Refrigerator-Friendly Badge */} + + + - - - - - - Show All - - - - - - - Refrigerator-Friendly Only - - - - - - - Not Refrigerator-Friendly Only - - - - + {isRefrigeratorFriendly(pantry) + ? 'Refrigerator-Friendly' + : 'Not Refrigerator-Friendly'} + - - )} - - - - {/* Empty State */} - {filteredAssignments.length === 0 && ( -
- - - No Assigned Pantries - - - You have no assigned pantries at this time. - -
- )} + - {/* Pantries Table */} - {filteredAssignments.length > 0 && ( - - - - - Pantry - - + - - - ); - }), - )} - - - )} - + View Orders + + + + ))} + + )}
); From db1dce869a5a123baa86914f42b4710e5cfcf5b9 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:53:58 -0500 Subject: [PATCH 08/12] amy comments --- apps/backend/src/auth/auth.controller.ts | 2 +- .../backend/src/auth/authenticated-request.ts | 2 +- apps/backend/src/auth/jwt.strategy.ts | 2 +- .../src/config/typeormTestDataSource.ts | 2 +- .../foodManufacturers/manufacturers.entity.ts | 2 +- .../manufacturers.service.ts | 2 +- .../src/pantries/pantries.controller.spec.ts | 2 +- apps/backend/src/pantries/pantries.entity.ts | 2 +- apps/backend/src/pantries/pantries.module.ts | 2 +- .../src/pantries/pantries.service.spec.ts | 2 +- apps/backend/src/pantries/pantries.service.ts | 2 +- .../src/users/users.controller.spec.ts | 2 +- apps/backend/src/users/users.controller.ts | 4 +- .../users/{user.entity.ts => users.entity.ts} | 0 apps/backend/src/users/users.module.ts | 2 +- apps/backend/src/users/users.service.spec.ts | 2 +- apps/backend/src/users/users.service.ts | 2 +- apps/backend/src/volunteers/types.ts | 3 + .../volunteers/volunteers.controller.spec.ts | 2 +- .../src/volunteers/volunteers.controller.ts | 7 +- .../src/volunteers/volunteers.module.ts | 2 +- .../src/volunteers/volunteers.service.spec.ts | 2 +- .../src/volunteers/volunteers.service.ts | 7 +- apps/frontend/src/api/apiClient.ts | 2 +- .../containers/volunteerAssignedPantries.tsx | 419 ++++++++++-------- 25 files changed, 268 insertions(+), 210 deletions(-) rename apps/backend/src/users/{user.entity.ts => users.entity.ts} (100%) create mode 100644 apps/backend/src/volunteers/types.ts diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index fd16bd9bf..a13828459 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -6,7 +6,7 @@ import { AuthService } from './auth.service'; import { UsersService } from '../users/users.service'; import { VerifyUserDto } from './dtos/verify-user.dto'; import { DeleteUserDto } from './dtos/delete-user.dto'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { SignInResponseDto } from './dtos/sign-in-response.dto'; import { RefreshTokenDto } from './dtos/refresh-token.dto'; import { ConfirmPasswordDto } from './dtos/confirm-password.dto'; diff --git a/apps/backend/src/auth/authenticated-request.ts b/apps/backend/src/auth/authenticated-request.ts index d8c0673f4..fc803c989 100644 --- a/apps/backend/src/auth/authenticated-request.ts +++ b/apps/backend/src/auth/authenticated-request.ts @@ -1,5 +1,5 @@ import { Request } from 'express'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; // user does not have to be provided by the client but is added automatically by the auth backend export interface AuthenticatedRequest extends Request { diff --git a/apps/backend/src/auth/jwt.strategy.ts b/apps/backend/src/auth/jwt.strategy.ts index 1c28aaf96..951be9def 100644 --- a/apps/backend/src/auth/jwt.strategy.ts +++ b/apps/backend/src/auth/jwt.strategy.ts @@ -5,7 +5,7 @@ import { ExtractJwt, Strategy } from 'passport-jwt'; import { UsersService } from '../users/users.service'; import CognitoAuthConfig from './aws-exports'; import { CognitoJwtPayload } from './jwt-payload.interface'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { diff --git a/apps/backend/src/config/typeormTestDataSource.ts b/apps/backend/src/config/typeormTestDataSource.ts index cfbc089e5..d1c0bd923 100644 --- a/apps/backend/src/config/typeormTestDataSource.ts +++ b/apps/backend/src/config/typeormTestDataSource.ts @@ -3,7 +3,7 @@ import { DataSource, DataSourceOptions } from 'typeorm'; import { PluralNamingStrategy } from '../strategies/plural-naming.strategy'; import { Order } from '../orders/order.entity'; import { Pantry } from '../pantries/pantries.entity'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { Donation } from '../donations/donations.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { FoodRequest } from '../foodRequests/request.entity'; diff --git a/apps/backend/src/foodManufacturers/manufacturers.entity.ts b/apps/backend/src/foodManufacturers/manufacturers.entity.ts index 8d3510520..3cff3e051 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.entity.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.entity.ts @@ -6,7 +6,7 @@ import { OneToMany, JoinColumn, } from 'typeorm'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { Donation } from '../donations/donations.entity'; import { Allergen, DonateWastedFood, ManufacturerAttribute } from './types'; import { ApplicationStatus } from '../shared/types'; diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts index 73a3a12db..ee4b49ac5 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts @@ -4,7 +4,7 @@ import { FoodManufacturer } from './manufacturers.entity'; import { Repository } from 'typeorm'; import { validateId } from '../utils/validation.utils'; import { FoodManufacturerApplicationDto } from './dtos/manufacturer-application.dto'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { Role } from '../users/types'; import { ApplicationStatus } from '../shared/types'; import { userSchemaDto } from '../users/dtos/userSchema.dto'; diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index 5e1f3bc9f..9a4829fb4 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -17,7 +17,7 @@ import { import { EmailsService } from '../emails/email.service'; import { ApplicationStatus } from '../shared/types'; import { NotFoundException, UnauthorizedException } from '@nestjs/common'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { AuthenticatedRequest } from '../auth/authenticated-request'; const mockPantriesService = mock(); diff --git a/apps/backend/src/pantries/pantries.entity.ts b/apps/backend/src/pantries/pantries.entity.ts index 395f14a0f..5760acfca 100644 --- a/apps/backend/src/pantries/pantries.entity.ts +++ b/apps/backend/src/pantries/pantries.entity.ts @@ -6,7 +6,7 @@ import { JoinColumn, ManyToMany, } from 'typeorm'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { Activity, AllergensConfidence, diff --git a/apps/backend/src/pantries/pantries.module.ts b/apps/backend/src/pantries/pantries.module.ts index 76a192fe0..64b5ca7ce 100644 --- a/apps/backend/src/pantries/pantries.module.ts +++ b/apps/backend/src/pantries/pantries.module.ts @@ -6,7 +6,7 @@ import { Pantry } from './pantries.entity'; import { AuthModule } from '../auth/auth.module'; import { OrdersModule } from '../orders/order.module'; import { EmailsModule } from '../emails/email.module'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { UsersModule } from '../users/users.module'; @Module({ diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts index 22e0a9add..aad4cf6ab 100644 --- a/apps/backend/src/pantries/pantries.service.spec.ts +++ b/apps/backend/src/pantries/pantries.service.spec.ts @@ -16,7 +16,7 @@ import { } from './types'; import { ApplicationStatus } from '../shared/types'; import { UsersService } from '../users/users.service'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { Role } from '../users/types'; const mockRepository = mock>(); diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts index 9c7cffe32..d21cd516e 100644 --- a/apps/backend/src/pantries/pantries.service.ts +++ b/apps/backend/src/pantries/pantries.service.ts @@ -7,7 +7,7 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { Pantry } from './pantries.entity'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { validateId } from '../utils/validation.utils'; import { ApplicationStatus } from '../shared/types'; import { PantryApplicationDto } from './dtos/pantry-application.dto'; diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 88a26ab51..0a2b0433b 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; -import { User } from './user.entity'; +import { User } from './users.entity'; import { Role } from './types'; import { userSchemaDto } from './dtos/userSchema.dto'; diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index 3384d0b39..820388711 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -11,15 +11,17 @@ import { Req, } from '@nestjs/common'; import { UsersService } from './users.service'; -import { User } from './user.entity'; +import { User } from './users.entity'; import { Role } from './types'; import { userSchemaDto } from './dtos/userSchema.dto'; import { AuthenticatedRequest } from '../auth/authenticated-request'; +import { Roles } from '../auth/roles.decorator'; @Controller('users') export class UsersController { constructor(private usersService: UsersService) {} + @Roles(Role.VOLUNTEER) @Get('/my-id') getCurrentUserId(@Req() req: AuthenticatedRequest): number { return req.user.id; diff --git a/apps/backend/src/users/user.entity.ts b/apps/backend/src/users/users.entity.ts similarity index 100% rename from apps/backend/src/users/user.entity.ts rename to apps/backend/src/users/users.entity.ts diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index 23177621e..f7bf1c194 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -2,7 +2,7 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; -import { User } from './user.entity'; +import { User } from './users.entity'; import { PantriesModule } from '../pantries/pantries.module'; import { AuthModule } from '../auth/auth.module'; diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 43a6fa8c0..4207622ef 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -3,7 +3,7 @@ import { Test } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { UsersService } from './users.service'; -import { User } from './user.entity'; +import { User } from './users.entity'; import { Role } from './types'; import { mock } from 'jest-mock-extended'; import { In } from 'typeorm'; diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index fa3e008e2..0377a6168 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -1,7 +1,7 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; -import { User } from './user.entity'; +import { User } from './users.entity'; import { Role } from './types'; import { validateId } from '../utils/validation.utils'; import { AuthService } from '../auth/auth.service'; diff --git a/apps/backend/src/volunteers/types.ts b/apps/backend/src/volunteers/types.ts new file mode 100644 index 000000000..0c167b2b2 --- /dev/null +++ b/apps/backend/src/volunteers/types.ts @@ -0,0 +1,3 @@ +import { User } from '../users/users.entity'; + +export type Assignments = Omit & { pantryIds: number[] }; diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts index 5a50f637d..8d4a17886 100644 --- a/apps/backend/src/volunteers/volunteers.controller.spec.ts +++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts @@ -2,7 +2,7 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; import { VolunteersController } from './volunteers.controller'; import { UsersController } from '../users/users.controller'; import { UsersService } from '../users/users.service'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { Role } from '../users/types'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index 4e7cc66ed..29763de83 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -6,11 +6,12 @@ import { Post, Body, } from '@nestjs/common'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { Pantry } from '../pantries/pantries.entity'; import { VolunteersService } from './volunteers.service'; import { Role } from '../users/types'; import { Roles } from '../auth/roles.decorator'; +import { Assignments } from './types'; @Controller('volunteers') export class VolunteersController { @@ -18,9 +19,7 @@ export class VolunteersController { @Roles(Role.ADMIN) @Get('/') - async getAllVolunteers(): Promise< - (Omit & { pantryIds: number[] })[] - > { + async getAllVolunteers(): Promise { return this.volunteersService.getVolunteersAndPantryAssignments(); } diff --git a/apps/backend/src/volunteers/volunteers.module.ts b/apps/backend/src/volunteers/volunteers.module.ts index c7147fcff..7f7a5d89a 100644 --- a/apps/backend/src/volunteers/volunteers.module.ts +++ b/apps/backend/src/volunteers/volunteers.module.ts @@ -1,6 +1,6 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { PantriesModule } from '../pantries/pantries.module'; import { AuthModule } from '../auth/auth.module'; import { VolunteersController } from './volunteers.controller'; diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index fb8f343bd..1bbcc7530 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -1,7 +1,7 @@ import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { VolunteersService } from './volunteers.service'; import { Pantry } from '../pantries/pantries.entity'; import { testDataSource } from '../config/typeormTestDataSource'; diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index ba6c5a10f..3cf9db186 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -1,12 +1,13 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { User } from '../users/user.entity'; +import { User } from '../users/users.entity'; import { Role } from '../users/types'; import { validateId } from '../utils/validation.utils'; import { Pantry } from '../pantries/pantries.entity'; import { PantriesService } from '../pantries/pantries.service'; import { UsersService } from '../users/users.service'; +import { Assignments } from './types'; @Injectable() export class VolunteersService { @@ -34,9 +35,7 @@ export class VolunteersService { return volunteer; } - async getVolunteersAndPantryAssignments(): Promise< - (Omit & { pantryIds: number[] })[] - > { + async getVolunteersAndPantryAssignments(): Promise { const volunteers = await this.usersService.findUsersByRoles([ Role.VOLUNTEER, ]); diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index ec93c4267..a81dd01a5 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -177,7 +177,7 @@ export class ApiClient { } public async getVolunteers(): Promise { - return this.get('/api/volunteers/') as Promise; + return this.get('/api/volunteers') as Promise; } public async getVolunteerPantries(userId: number): Promise { diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index 057237d9c..c9d3924b5 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -8,6 +8,7 @@ import { VStack, RadioGroup, Text, + Spinner, } from '@chakra-ui/react'; import ApiClient from '@api/apiClient'; import { Pantry } from 'types/types'; @@ -18,6 +19,7 @@ import { useNavigate } from 'react-router-dom'; const AssignedPantries: React.FC = () => { const navigate = useNavigate(); const [pantries, setPantries] = useState([]); + const [isLoading, setIsLoading] = useState(true); const [isFilterOpen, setIsFilterOpen] = useState(false); const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = useState< boolean | null @@ -26,13 +28,22 @@ const AssignedPantries: React.FC = () => { useEffect(() => { const fetchAssignedPantries = async () => { + let userId: number; + try { + userId = await ApiClient.getMyId(); + } catch { + setAlertMessage('Authentication error. Please log in and try again.'); + setIsLoading(false); + return; + } + try { - const userId = await ApiClient.getMyId(); const data = await ApiClient.getVolunteerPantries(userId); setPantries(data); - } catch (error) { - console.error('Error fetching assigned pantries:', error); + } catch { setAlertMessage('Error fetching assigned pantries'); + } finally { + setIsLoading(false); } }; @@ -53,6 +64,10 @@ const AssignedPantries: React.FC = () => { ); }, [filterRefrigeratorFriendly, pantries]); + const hasNoAssignedPantries = !isLoading && pantries.length === 0; + const hasNoFilterResults = + !isLoading && pantries.length > 0 && filteredPantries.length === 0; + // Map radio value string to filter state const radioValue = filterRefrigeratorFriendly === true @@ -67,6 +82,11 @@ const AssignedPantries: React.FC = () => { else setFilterRefrigeratorFriendly(null); }; + const clearFilter = () => { + setFilterRefrigeratorFriendly(null); + setIsFilterOpen(false); + }; + const tableHeaderStyles = { borderBottom: '1px solid', borderColor: 'neutral.100', @@ -88,195 +108,230 @@ const AssignedPantries: React.FC = () => { Assigned Pantries - {/* Filter Button */} - - - - - {isFilterOpen && ( - <> - setIsFilterOpen(false)} - zIndex={10} - /> - - - handleRadioChange(e.value) - } - size="sm" - > - - - - - - Refrigerator-Friendly Only - - - - - - - Not Refrigerator-Friendly Only - - - - - - - )} - - - - {/* Pantries Table */} - {filteredPantries.length === 0 ? ( - - - - No Assigned Pantries - - - You have no assigned pantries at this time. - + {isLoading ? ( + + ) : ( - - - - - Pantry - - - Refrigerator-Friendly - - - Action - - - - - {filteredPantries.map((pantry) => ( - - {/* Pantry Name */} - + {/* Filter Button — only shown when pantries exist */} + {!hasNoAssignedPantries && ( + + + - {/* Refrigerator-Friendly Badge */} - - + {isFilterOpen && ( + <> setIsFilterOpen(false)} + zIndex={10} + /> + - {isRefrigeratorFriendly(pantry) - ? 'Refrigerator-Friendly' - : 'Not Refrigerator-Friendly'} + + handleRadioChange(e.value) + } + size="sm" + > + + + + + + Refrigerator-Friendly Only + + + + + + + Not Refrigerator-Friendly Only + + + + + {filterRefrigeratorFriendly !== null && ( + + )} - - + + )} + + + )} + + {/* Empty States */} + {(hasNoAssignedPantries || hasNoFilterResults) && ( + + + {hasNoAssignedPantries ? ( + <> + + No Assigned Pantries + + + You have no assigned pantries at this time. + + + ) : ( + <> + + No Matching Pantries + + + No pantries match the current filter. + + + )} + + )} - {/* Action */} - - - - - ))} - - + Refrigerator-Friendly + + + Action + + + + + {filteredPantries.map((pantry) => ( + + {/* Pantry Name */} + + + {pantry.pantryName} + + + + {/* Refrigerator-Friendly Badge */} + + + + {isRefrigeratorFriendly(pantry) + ? 'Refrigerator-Friendly' + : 'Not Refrigerator-Friendly'} + + + + + {/* Action */} + + + + + ))} + + + )} + )} ); From 81f3b442a624264fb5b5edd757801faf6956102c Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Fri, 6 Mar 2026 18:17:38 -0500 Subject: [PATCH 09/12] fix return type --- apps/frontend/src/api/apiClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index a81dd01a5..baa03f910 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -177,7 +177,7 @@ export class ApiClient { } public async getVolunteers(): Promise { - return this.get('/api/volunteers') as Promise; + return this.get('/api/volunteers') as Promise; } public async getVolunteerPantries(userId: number): Promise { From 4bb9bada7f53a9b22bf3467613f71d97e1302482 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:47:50 -0500 Subject: [PATCH 10/12] fix filter, not null assertions --- .../src/volunteers/volunteers.service.ts | 6 +- apps/frontend/src/api/apiClient.ts | 4 +- .../containers/volunteerAssignedPantries.tsx | 175 ++++++++++++------ 3 files changed, 119 insertions(+), 66 deletions(-) diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index 3cf9db186..32429296b 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -44,7 +44,7 @@ export class VolunteersService { const { pantries, ...volunteerWithoutPantries } = v; return { ...volunteerWithoutPantries, - pantryIds: pantries!.map((p) => p.pantryId), + pantryIds: pantries?.map((p) => p.pantryId) || [], }; }); } @@ -52,7 +52,7 @@ export class VolunteersService { async getVolunteerPantries(volunteerId: number): Promise { validateId(volunteerId, 'Volunteer'); const volunteer = await this.findOne(volunteerId); - return volunteer.pantries!; + return volunteer.pantries || []; } async assignPantriesToVolunteer( @@ -64,7 +64,7 @@ export class VolunteersService { const volunteer = await this.findOne(volunteerId); const pantries = await this.pantriesService.findByIds(pantryIds); - const existingPantries = volunteer.pantries!; + const existingPantries = volunteer.pantries || []; const existingPantryIds = existingPantries.map((p) => p.pantryId); const newPantries = pantries.filter( (p) => !existingPantryIds.includes(p.pantryId), diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index baa03f910..534d377a2 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -176,8 +176,8 @@ export class ApiClient { .then((response) => response.data); } - public async getVolunteers(): Promise { - return this.get('/api/volunteers') as Promise; + public async getVolunteers(): Promise { + return this.get('/api/volunteers') as Promise; } public async getVolunteerPantries(userId: number): Promise { diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index c9d3924b5..a209c45a9 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -6,9 +6,10 @@ import { Table, Heading, VStack, - RadioGroup, + Checkbox, Text, Spinner, + Input, } from '@chakra-ui/react'; import ApiClient from '@api/apiClient'; import { Pantry } from 'types/types'; @@ -21,9 +22,10 @@ const AssignedPantries: React.FC = () => { const [pantries, setPantries] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isFilterOpen, setIsFilterOpen] = useState(false); - const [filterRefrigeratorFriendly, setFilterRefrigeratorFriendly] = useState< - boolean | null - >(null); + const [selectedPantryIds, setSelectedPantryIds] = useState>( + new Set(), + ); + const [pantrySearch, setPantrySearch] = useState(''); const [alertMessage, setAlertMessage] = useState(null); useEffect(() => { @@ -58,35 +60,44 @@ const AssignedPantries: React.FC = () => { }; const filteredPantries = useMemo(() => { - if (filterRefrigeratorFriendly === null) return pantries; - return pantries.filter( - (pantry) => isRefrigeratorFriendly(pantry) === filterRefrigeratorFriendly, - ); - }, [filterRefrigeratorFriendly, pantries]); + if (selectedPantryIds.size === 0) return pantries; + return pantries.filter((pantry) => selectedPantryIds.has(pantry.pantryId)); + }, [selectedPantryIds, pantries]); const hasNoAssignedPantries = !isLoading && pantries.length === 0; const hasNoFilterResults = !isLoading && pantries.length > 0 && filteredPantries.length === 0; - // Map radio value string to filter state - const radioValue = - filterRefrigeratorFriendly === true - ? 'yes' - : filterRefrigeratorFriendly === false - ? 'no' - : ''; + const isFiltered = selectedPantryIds.size > 0; - const handleRadioChange = (value: string) => { - if (value === 'yes') setFilterRefrigeratorFriendly(true); - else if (value === 'no') setFilterRefrigeratorFriendly(false); - else setFilterRefrigeratorFriendly(null); + const togglePantry = (pantryId: number) => { + setSelectedPantryIds((prev) => { + const next = new Set(prev); + if (next.has(pantryId)) next.delete(pantryId); + else next.add(pantryId); + return next; + }); }; - const clearFilter = () => { - setFilterRefrigeratorFriendly(null); - setIsFilterOpen(false); + const allChecked = + pantries.length > 0 && selectedPantryIds.size === pantries.length; + const isIndeterminate = selectedPantryIds.size > 0 && !allChecked; + + const toggleAll = () => { + if (allChecked || isIndeterminate) { + setSelectedPantryIds(new Set()); + } else { + setSelectedPantryIds(new Set(pantries.map((p) => p.pantryId))); + } }; + const visiblePantries = useMemo(() => { + if (!pantrySearch.trim()) return pantries; + return pantries.filter((p) => + p.pantryName.toLowerCase().includes(pantrySearch.toLowerCase()), + ); + }, [pantrySearch, pantries]); + const tableHeaderStyles = { borderBottom: '1px solid', borderColor: 'neutral.100', @@ -114,16 +125,16 @@ const AssignedPantries: React.FC = () => { ) : ( <> - {/* Filter Button — only shown when pantries exist */} {!hasNoAssignedPantries && ( {isFilterOpen && ( @@ -158,43 +183,71 @@ const AssignedPantries: React.FC = () => { minW="275px" zIndex={20} > - - handleRadioChange(e.value) - } - size="sm" - > - - - - - - Refrigerator-Friendly Only - - - - - - - Not Refrigerator-Friendly Only - - - - - {filterRefrigeratorFriendly !== null && ( - - )} + + + + + Select All + + + + + + + {visiblePantries.length > 0 ? ( + visiblePantries.map((pantry) => ( + + togglePantry(pantry.pantryId) + } + size="sm" + > + + + + + {pantry.pantryName} + + + + )) + ) : ( + + No pantries found. + + )} + )} From e80376b85859645388ceb2defaa6a1e523345ef9 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Sat, 7 Mar 2026 10:30:59 -0500 Subject: [PATCH 11/12] amy comments --- .../frontend/src/containers/volunteerAssignedPantries.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/frontend/src/containers/volunteerAssignedPantries.tsx b/apps/frontend/src/containers/volunteerAssignedPantries.tsx index a209c45a9..a89318958 100644 --- a/apps/frontend/src/containers/volunteerAssignedPantries.tsx +++ b/apps/frontend/src/containers/volunteerAssignedPantries.tsx @@ -268,19 +268,19 @@ const AssignedPantries: React.FC = () => { {hasNoAssignedPantries ? ( <> - + No Assigned Pantries - + You have no assigned pantries at this time. ) : ( <> - + No Matching Pantries - + No pantries match the current filter. From 7610f00f0b4b70ab5d18b69cb8365ca09942a527 Mon Sep 17 00:00:00 2001 From: Max Norman <27299284+maxn990@users.noreply.github.com> Date: Mon, 9 Mar 2026 19:08:17 -0400 Subject: [PATCH 12/12] final comment --- apps/backend/src/users/users.controller.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index 820388711..1a9963e5c 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -15,13 +15,14 @@ import { User } from './users.entity'; import { Role } from './types'; import { userSchemaDto } from './dtos/userSchema.dto'; import { AuthenticatedRequest } from '../auth/authenticated-request'; -import { Roles } from '../auth/roles.decorator'; +import { JwtAuthGuard } from '../auth/jwt-auth.guard'; +import { UseGuards } from '@nestjs/common'; @Controller('users') export class UsersController { constructor(private usersService: UsersService) {} - @Roles(Role.VOLUNTEER) + @UseGuards(JwtAuthGuard) @Get('/my-id') getCurrentUserId(@Req() req: AuthenticatedRequest): number { return req.user.id;