From ca3abb7c0d1c4feb56d21a8680ce11dbefdc2414 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Thu, 30 Apr 2026 18:10:48 -0400 Subject: [PATCH 1/7] final commit --- apps/backend/src/orders/order.controller.ts | 4 +- .../volunteers/volunteers.controller.spec.ts | 12 +- .../src/volunteers/volunteers.controller.ts | 22 +- apps/frontend/src/api/apiClient.ts | 15 +- apps/frontend/src/app.tsx | 18 ++ apps/frontend/src/components/Header.tsx | 3 +- .../src/components/foodRequestManagement.tsx | 8 + .../components/forms/resetPasswordModal.tsx | 5 +- .../src/components/protectedRoute.tsx | 3 +- .../containers/approveFoodManufacturers.tsx | 6 +- .../src/containers/approvePantries.tsx | 6 +- apps/frontend/src/containers/formRequests.tsx | 12 + apps/frontend/src/containers/homepage.tsx | 57 +++-- apps/frontend/src/containers/loginPage.tsx | 4 +- .../src/containers/pantryDashboard.tsx | 236 ++++++++---------- .../src/containers/pantryOrderManagement.tsx | 15 +- apps/frontend/src/containers/unauthorized.tsx | 4 +- .../src/containers/volunteerDashboard.tsx | 126 ++++++++++ .../src/containers/volunteerManagement.tsx | 3 +- .../containers/volunteerOrderManagement.tsx | 12 + .../containers/volunteerRequestManagement.tsx | 18 +- apps/frontend/src/routes.ts | 2 + 22 files changed, 404 insertions(+), 187 deletions(-) create mode 100644 apps/frontend/src/containers/volunteerDashboard.tsx diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index a43649640..134457722 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -82,8 +82,8 @@ export class OrdersController { resolver: async ({ entityId, services }) => { return pipeNullable( () => services.get(OrdersService).findOrderFoodRequest(entityId), - (request: FoodRequest) => - services.get(PantriesService).findOne(request.pantryId), + (request: FoodRequestSummaryDto) => + services.get(PantriesService).findOne(request.pantry.pantryId), (pantry: Pantry) => [pantry.pantryUser.id], ); }, diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts index b2868620f..1691952d7 100644 --- a/apps/backend/src/volunteers/volunteers.controller.spec.ts +++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts @@ -214,8 +214,11 @@ describe('VolunteersController', () => { }); }); - describe('GET /:id/my-recent-orders', () => { + describe('GET /me/my-recent-orders', () => { it('returns the 2 most recent orders for a volunteer', async () => { + const req: AuthenticatedRequest = { + user: { id: 6 }, + } as AuthenticatedRequest; const assignee = { id: 6, firstName: 'James', lastName: 'Thomas' }; const recentOrders: Partial[] = [ { @@ -236,7 +239,7 @@ describe('VolunteersController', () => { recentOrders as VolunteerOrder[], ); - const result = await controller.getRecentOrders(6); + const result = await controller.getRecentOrders(req); expect(result).toEqual(recentOrders); expect(result).toHaveLength(2); @@ -244,9 +247,12 @@ describe('VolunteersController', () => { }); it('returns empty array when volunteer has no assigned orders', async () => { + const req: AuthenticatedRequest = { + user: { id: 6 }, + } as AuthenticatedRequest; mockVolunteersService.getRecentOrders.mockResolvedValueOnce([]); - const result = await controller.getRecentOrders(6); + const result = await controller.getRecentOrders(req); expect(result).toEqual([]); expect(mockVolunteersService.getRecentOrders).toHaveBeenCalledWith(6); diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index ee330b44e..5dc36a729 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -16,7 +16,6 @@ import { Assignments, VolunteerOrder } from './types'; import { AuthenticatedRequest } from '../auth/authenticated-request'; import { OrdersService } from '../orders/order.service'; import { FoodRequestSummaryDto } from '../foodRequests/dtos/food-request-summary.dto'; -import { CheckOwnership } from '../auth/ownership.decorator'; @Controller('volunteers') export class VolunteersController { @@ -44,19 +43,6 @@ export class VolunteersController { return this.volunteersService.findOne(userId); } - @CheckOwnership({ - idParam: 'id', - resolver: async ({ entityId }) => [entityId], - bypassRoles: [Role.ADMIN], - }) - @Roles(Role.VOLUNTEER, Role.ADMIN) - @Get('/:id/my-recent-orders') - async getRecentOrders( - @Param('id', ParseIntPipe) id: number, - ): Promise { - return this.volunteersService.getRecentOrders(id); - } - @Post('/:id/pantries') async assignPantries( @Param('id', ParseIntPipe) id: number, @@ -75,6 +61,14 @@ export class VolunteersController { return this.volunteersService.findRequestsByVolunteer(currentUser.id); } + @Roles(Role.VOLUNTEER) + @Get('/me/my-recent-orders') + async getRecentOrders( + @Req() req: AuthenticatedRequest, + ): Promise { + return this.volunteersService.getRecentOrders(req.user.id); + } + // returns all orders globally // only includes actionCompletion for orders assigned to the requesting volunteer @Roles(Role.VOLUNTEER) diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index d9ca8653b..c6941ee1a 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -6,6 +6,7 @@ import axios, { type InternalAxiosRequestConfig, } from 'axios'; import { NavigateFunction } from 'react-router-dom'; +import { ROUTES } from '../routes'; import { User, Order, @@ -76,9 +77,9 @@ export class ApiClient { (error: AxiosError) => { if (error.response?.status === 403) { if (this.navigate) { - this.navigate('/unauthorized'); + this.navigate(ROUTES.UNAUTHORIZED); } else { - window.location.replace('/unauthorized'); + window.location.replace(ROUTES.UNAUTHORIZED); } } return Promise.reject(error); @@ -169,9 +170,7 @@ export class ApiClient { .then((response) => response.data); } - public async getPantryOrders( - pantryId: number, - ): Promise { + public async getPantryOrders(pantryId: number): Promise { return this.axiosInstance .get(`/api/pantries/${pantryId}/orders`) .then((response) => response.data); @@ -255,6 +254,12 @@ export class ApiClient { .then((response) => response.data); } + public async getVolunteerRecentOrders(): Promise { + return this.axiosInstance + .get(`/api/volunteers/me/my-recent-orders`) + .then((response) => response.data); + } + public async completeOrderAction( orderId: number, action: VolunteerAction, diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index 5a7efaa9d..3683014a8 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -33,6 +33,8 @@ import ProfilePage from '@containers/profilePage'; import VolunteerOrderManagement from '@containers/volunteerOrderManagement'; import TestAdminDashboard from '@containers/testAdminDashboard'; import AdminRequestManagement from '@containers/adminRequestManagement'; +import PantryDashboard from '@containers/pantryDashboard'; +import VolunteerDashboard from '@containers/volunteerDashboard'; Amplify.configure(CognitoAuthConfig); @@ -85,6 +87,22 @@ const router = createBrowserRouter([ ), }, + { + path: ROUTES.PANTRY_DASHBOARD, + element: ( + + + + ), + }, + { + path: ROUTES.VOLUNTEER_DASHBOARD, + element: ( + + + + ), + }, { path: ROUTES.FM_DONATION_MANAGEMENT, element: ( diff --git a/apps/frontend/src/components/Header.tsx b/apps/frontend/src/components/Header.tsx index d1f76a8a8..298f18b8c 100644 --- a/apps/frontend/src/components/Header.tsx +++ b/apps/frontend/src/components/Header.tsx @@ -2,6 +2,7 @@ import React from 'react'; import SignOutButton from './signOutButton'; import { useAuthenticator } from '@aws-amplify/ui-react'; import { Link as RouterLink } from 'react-router-dom'; +import { ROUTES } from '../routes'; import { Link } from '@chakra-ui/react'; const Header = () => { @@ -10,7 +11,7 @@ const Header = () => { return (
- Securing Safe Food + Securing Safe Food {user && } diff --git a/apps/frontend/src/components/foodRequestManagement.tsx b/apps/frontend/src/components/foodRequestManagement.tsx index 805df96f3..169b16168 100644 --- a/apps/frontend/src/components/foodRequestManagement.tsx +++ b/apps/frontend/src/components/foodRequestManagement.tsx @@ -24,11 +24,13 @@ import { useAlert } from '../hooks/alert'; interface RequestManagementProps { fetchRequests: () => Promise; enableVolunteerActions?: boolean; + initialRequestId?: number; } const RequestManagement: React.FC = ({ fetchRequests: fetchData, enableVolunteerActions = true, + initialRequestId, }) => { const [requests, setRequests] = useState([]); const [sortRequestedAtAsc, setSortRequestedAtAsc] = useState(false); @@ -68,6 +70,12 @@ const RequestManagement: React.FC = ({ setCurrentPage(1); }, [selectedFilteredPantries]); + useEffect(() => { + if (!initialRequestId || requests.length === 0) return; + const match = requests.find((r) => r.requestId === initialRequestId); + if (match) setSelectedViewDetailsRequest(match); + }, [initialRequestId, requests]); + const pantryOptions = [ ...new Set( requests diff --git a/apps/frontend/src/components/forms/resetPasswordModal.tsx b/apps/frontend/src/components/forms/resetPasswordModal.tsx index e87df83c0..5fa1b0943 100644 --- a/apps/frontend/src/components/forms/resetPasswordModal.tsx +++ b/apps/frontend/src/components/forms/resetPasswordModal.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; +import { ROUTES } from '../../routes'; import { Box, Text, @@ -59,7 +60,7 @@ const ResetPasswordModal: React.FC = () => { confirmationCode: code, newPassword: password, }); - navigate('/login'); + navigate(ROUTES.LOGIN); } catch { setAlertMessage('Failed to set new password'); } @@ -205,7 +206,7 @@ const ResetPasswordModal: React.FC = () => { navigate('/login')} + onClick={() => navigate(ROUTES.LOGIN)} variant="underline" textDecorationColor="neutral.300" > diff --git a/apps/frontend/src/components/protectedRoute.tsx b/apps/frontend/src/components/protectedRoute.tsx index cdd6ed231..52392dd3c 100644 --- a/apps/frontend/src/components/protectedRoute.tsx +++ b/apps/frontend/src/components/protectedRoute.tsx @@ -1,4 +1,5 @@ import { Navigate, useLocation, Outlet } from 'react-router-dom'; +import { ROUTES } from '../routes'; import { useAuthenticator } from '@aws-amplify/ui-react'; import { Center, Spinner, Text } from '@chakra-ui/react'; @@ -20,7 +21,7 @@ const ProtectedRoute = ({ children }: Props) => { } if (authStatus !== 'authenticated') { - return ; + return ; } return children ?? ; diff --git a/apps/frontend/src/containers/approveFoodManufacturers.tsx b/apps/frontend/src/containers/approveFoodManufacturers.tsx index 17627f8a3..74015df29 100644 --- a/apps/frontend/src/containers/approveFoodManufacturers.tsx +++ b/apps/frontend/src/containers/approveFoodManufacturers.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { ROUTES } from '../routes'; import { Table, Button, @@ -322,7 +323,10 @@ const ApproveFoodManufacturers: React.FC = () => { textStyle="p2" variant="underline" textDecorationColor="neutral.700" - href={`/food-manufacturer-application-details/${foodManufacturer.foodManufacturerId}`} + href={ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS.replace( + ':applicationId', + String(foodManufacturer.foodManufacturerId), + )} > View Details diff --git a/apps/frontend/src/containers/approvePantries.tsx b/apps/frontend/src/containers/approvePantries.tsx index 3d1bdf80e..ef4941d3c 100644 --- a/apps/frontend/src/containers/approvePantries.tsx +++ b/apps/frontend/src/containers/approvePantries.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; +import { ROUTES } from '../routes'; import { Table, Button, @@ -308,7 +309,10 @@ const ApprovePantries: React.FC = () => { textStyle="p2" variant="underline" textDecorationColor="neutral.700" - href={`/pantry-application-details/${pantry.pantryId}`} + href={ROUTES.PANTRY_APPLICATION_DETAILS.replace( + ':applicationId', + String(pantry.pantryId), + )} > View Details diff --git a/apps/frontend/src/containers/formRequests.tsx b/apps/frontend/src/containers/formRequests.tsx index 7adba8da6..d9d21251c 100644 --- a/apps/frontend/src/containers/formRequests.tsx +++ b/apps/frontend/src/containers/formRequests.tsx @@ -25,6 +25,7 @@ import { formatDate } from '@utils/utils'; import ApiClient from '@api/apiClient'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; +import { useSearchParams } from 'react-router-dom'; const FormRequests: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); @@ -40,6 +41,7 @@ const FormRequests: React.FC = () => { const [openReadOnlyRequest, setOpenReadOnlyRequest] = useState(null); + const [searchParams] = useSearchParams(); const [alertState, setAlertMessage] = useAlert(); const pageSize = 10; @@ -69,6 +71,16 @@ const FormRequests: React.FC = () => { fetchRequests(); }, [fetchRequests]); + useEffect(() => { + const requestIdFromUrl = searchParams.get('requestId'); + if (!requestIdFromUrl || requests.length === 0) return; + + const match = requests.find( + (r) => r.requestId === Number(requestIdFromUrl), + ); + if (match) setOpenReadOnlyRequest(match); + }, [searchParams, requests]); + const paginatedRequests = requests.slice( (currentPage - 1) * pageSize, currentPage * pageSize, diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index 26750cb02..79ac37e7d 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Link as RouterLink } from 'react-router-dom'; +import { ROUTES } from '../routes'; import { Box, Container, @@ -25,7 +26,7 @@ const Homepage: React.FC = () => { - Profile View + Profile View @@ -35,19 +36,24 @@ const Homepage: React.FC = () => { - Request Form + Dasboard - + Request Form + + + + + Pantry Application - + Order Management @@ -62,14 +68,14 @@ const Homepage: React.FC = () => { - + Donation Management - + Food Manufacturer Application @@ -84,21 +90,28 @@ const Homepage: React.FC = () => { - + + Dashboard + + + + + + Assigned Pantries - + Food Request Management - + Order Management @@ -113,12 +126,14 @@ const Homepage: React.FC = () => { - Approve Pantries + + Approve Pantries + - + Approve Food Manufacturers @@ -129,41 +144,43 @@ const Homepage: React.FC = () => { - - + + Volunteer Management - + Donation Management - + Donation Statistics - + Order Management - Dashboard + + Dashboard + - + Food Request Management @@ -179,12 +196,12 @@ const Homepage: React.FC = () => { - Login + Login - Sign Up + Sign Up diff --git a/apps/frontend/src/containers/loginPage.tsx b/apps/frontend/src/containers/loginPage.tsx index 1cc951dfd..037d6f711 100644 --- a/apps/frontend/src/containers/loginPage.tsx +++ b/apps/frontend/src/containers/loginPage.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { ROUTES } from '../routes'; import { signIn, confirmSignIn, fetchAuthSession } from '@aws-amplify/auth'; import { useNavigate, useLocation } from 'react-router-dom'; import { @@ -17,7 +18,6 @@ import { Eye, EyeOff } from 'lucide-react'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; import AuthHeader from '@components/AuthHeader'; -import { ROUTES } from '../routes'; type Step = 'login' | 'new-password'; @@ -34,7 +34,7 @@ const LoginPage: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); - const from = location.state?.from?.pathname || '/'; + const from = location.state?.from?.pathname || ROUTES.HOME; const handleLogin = async () => { try { diff --git a/apps/frontend/src/containers/pantryDashboard.tsx b/apps/frontend/src/containers/pantryDashboard.tsx index 95595aa6b..56cec6e0d 100644 --- a/apps/frontend/src/containers/pantryDashboard.tsx +++ b/apps/frontend/src/containers/pantryDashboard.tsx @@ -1,147 +1,131 @@ -import { - Menu, - Portal, - Button, - HStack, - Text, - VStack, - Card, - CardBody, - Box, - Link, -} from '@chakra-ui/react'; -import { MenuIcon } from 'lucide-react'; import React, { useEffect, useState } from 'react'; -import { PantryWithUser } from 'types/types'; -import { formatPhone } from '@utils/utils'; +import { Box, Heading, Text } from '@chakra-ui/react'; +import DashboardCard, { ORDER_STATUS_BADGE } from '@components/dashboardCard'; +import { + FoodRequestSummaryDto, + OrderSummary, + PantryWithUser, +} from '../types/types'; +import { DashboardCardType } from '@components/dashboardCard'; import ApiClient from '@api/apiClient'; +import { useAlert } from '../hooks/alert'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useNavigate } from 'react-router-dom'; +import { ROUTES } from '../routes'; const PantryDashboard: React.FC = () => { - const [pantryId, setPantryId] = useState(null); + const navigate = useNavigate(); + + const [alertState, setAlertMessage] = useAlert(); const [pantry, setPantry] = useState(null); + const [recentFoodRequests, setRecentFoodRequests] = useState< + FoodRequestSummaryDto[] + >([]); + const [recentOrders, setRecentOrders] = useState([]); - useEffect(() => { - const fetchPantryId = async () => { - try { - const pantryId = await ApiClient.getCurrentUserPantryId(); - setPantryId(pantryId); - } catch (error) { - console.error('Error fetching pantry ID', error); - } - }; + const fetchRecentFoodRequests = async (pantryId: number) => { + try { + const pantryFoodRequests = await ApiClient.getPantryRequests(pantryId); + const sortedFoodRequests = pantryFoodRequests.sort( + (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => + new Date(b.requestedAt).getTime() - new Date(a.requestedAt).getTime(), + ); + setRecentFoodRequests(sortedFoodRequests.slice(0, 2)); + } catch { + setAlertMessage('Error fetching pantry food requests'); + } + }; - fetchPantryId(); - }, []); + const fetchRecentOrders = async (pantryId: number) => { + try { + const pantryOrders = await ApiClient.getPantryOrders(pantryId); + const sortedOrders = pantryOrders.sort( + (a: OrderSummary, b: OrderSummary) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + setRecentOrders(sortedOrders.slice(0, 4)); + } catch { + setAlertMessage('Error fetching orders'); + } + }; useEffect(() => { - const fetchPantryData = async () => { - if (!pantryId) return; - + const fetchDashboardData = async () => { + let pantryData: PantryWithUser; try { - const pantryData = await ApiClient.getPantry(pantryId); + const pantryId = await ApiClient.getCurrentUserPantryId(); + pantryData = await ApiClient.getPantry(pantryId); setPantry(pantryData); - } catch (error) { - console.error('Error fetching pantry data/SSFRep data', error); + } catch { + setAlertMessage('Error fetching pantry information'); + return; } + fetchRecentFoodRequests(pantryData.pantryId); + fetchRecentOrders(pantryData.pantryId); }; + fetchDashboardData(); + }, [setAlertMessage]); - fetchPantryData(); - }, [pantryId]); + if (!pantry) return; return ( - - - - Welcome {pantry?.pantryName}! - - - - - - - - - - - Profile - - - Request Form - - - Sign out - - - - - - - + + {alertState && ( + + )} + + Welcome, {pantry.pantryName} + - - - - Need help? Contact your SSF representative - - - Name: {pantry?.pantryUser?.firstName} {pantry?.pantryUser?.lastName} - - Email: {pantry?.pantryUser?.email} - Phone: {formatPhone(pantry?.pantryUser?.phone)} - - + + Recent Food Requests + + + {recentFoodRequests.map((fr) => ( + + navigate(`${ROUTES.REQUEST_FORM}?requestId=${fr.requestId}`) + } + /> + ))} + - - + + Recent Orders + + + {recentOrders.map((order) => ( + + navigate( + `${ROUTES.PANTRY_ORDER_MANAGEMENT}?orderId=${order.orderId}`, + ) + } + /> + ))} + + ); }; diff --git a/apps/frontend/src/containers/pantryOrderManagement.tsx b/apps/frontend/src/containers/pantryOrderManagement.tsx index baf28ef4e..94118e1f2 100644 --- a/apps/frontend/src/containers/pantryOrderManagement.tsx +++ b/apps/frontend/src/containers/pantryOrderManagement.tsx @@ -18,13 +18,14 @@ import { } from 'lucide-react'; import { capitalize, formatDate, ORDER_STATUS_COLORS } from '@utils/utils'; import ApiClient from '@api/apiClient'; -import { OrderStatus, OrderWithoutFoodManufacturer } from '../types/types'; +import { OrderStatus, OrderSummary } from '../types/types'; import OrderReceivedActionModal from '@components/forms/orderReceivedActionModal'; import OrderDetailsModal from '@components/forms/orderDetailsModal'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; +import { useSearchParams } from 'react-router-dom'; -type OrderWithColor = OrderWithoutFoodManufacturer & { assigneeColor?: string }; +type OrderWithColor = OrderSummary & { assigneeColor?: string }; const MAX_PER_STATUS = 5; const PantryOrderManagement: React.FC = () => { @@ -55,6 +56,7 @@ const PantryOrderManagement: React.FC = () => { const [isAlertError, setIsAlertError] = useState(false); const [alertState, setAlertMessage] = useAlert(); + const [searchParams] = useSearchParams(); // State to hold filter state per status type FilterState = { @@ -116,6 +118,15 @@ const PantryOrderManagement: React.FC = () => { fetchOrders(); }, []); + useEffect(() => { + const orderIdFromUrl = searchParams.get('orderId'); + const allOrders = Object.values(statusOrders).flat(); + if (!orderIdFromUrl || allOrders.length === 0) return; + + const match = allOrders.find((o) => o.orderId === Number(orderIdFromUrl)); + if (match) setSelectedOrderId(match.orderId); + }, [searchParams, statusOrders]); + // Helper to reset page for a specific status const resetPageForStatus = (status: OrderStatus) => { setCurrentPages((prev) => ({ ...prev, [status]: 1 })); diff --git a/apps/frontend/src/containers/unauthorized.tsx b/apps/frontend/src/containers/unauthorized.tsx index 39dc5d844..d38221f8d 100644 --- a/apps/frontend/src/containers/unauthorized.tsx +++ b/apps/frontend/src/containers/unauthorized.tsx @@ -1,3 +1,5 @@ +import { ROUTES } from '../routes'; + export const Unauthorized: React.FC = () => { return (
@@ -6,7 +8,7 @@ export const Unauthorized: React.FC = () => {

Return to{' '} - home page + home page

diff --git a/apps/frontend/src/containers/volunteerDashboard.tsx b/apps/frontend/src/containers/volunteerDashboard.tsx new file mode 100644 index 000000000..2075c19e3 --- /dev/null +++ b/apps/frontend/src/containers/volunteerDashboard.tsx @@ -0,0 +1,126 @@ +import React, { useEffect, useState } from 'react'; +import { Box, Heading, Text } from '@chakra-ui/react'; +import DashboardCard, { ORDER_STATUS_BADGE } from '@components/dashboardCard'; +import { FoodRequestSummaryDto, User, VolunteerOrder } from '../types/types'; +import { DashboardCardType } from '@components/dashboardCard'; +import ApiClient from '@api/apiClient'; +import { useAlert } from '../hooks/alert'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useNavigate } from 'react-router-dom'; +import { ROUTES } from '../routes'; + +const VolunteerDashboard: React.FC = () => { + const navigate = useNavigate(); + + const [alertState, setAlertMessage] = useAlert(); + const [user, setUser] = useState(null); + const [recentFoodRequests, setRecentFoodRequests] = useState< + FoodRequestSummaryDto[] + >([]); + const [recentOrders, setRecentOrders] = useState([]); + + const fetchRecentFoodRequests = async () => { + try { + const requests = await ApiClient.getVolunteerAssignedRequests(); + const sorted = requests.sort( + (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => + new Date(b.requestedAt).getTime() - new Date(a.requestedAt).getTime(), + ); + setRecentFoodRequests(sorted.slice(0, 2)); + } catch { + setAlertMessage('Error fetching food requests'); + } + }; + + const fetchRecentOrders = async () => { + try { + const orders = await ApiClient.getVolunteerRecentOrders(); + setRecentOrders(orders); + } catch { + setAlertMessage('Error fetching orders'); + } + }; + + useEffect(() => { + const fetchDashboardData = async () => { + try { + const currentUser = await ApiClient.getMe(); + setUser(currentUser); + } catch { + setAlertMessage('Error fetching user information'); + return; + } + fetchRecentFoodRequests(); + fetchRecentOrders(); + }; + fetchDashboardData(); + }, [setAlertMessage]); + + if (!user) return null; + + return ( + + {alertState && ( + + )} + + Welcome, {user.firstName} {user.lastName} + + + + Recent Food Requests + + + {recentFoodRequests.map((fr) => ( + + navigate( + `${ROUTES.VOLUNTEER_REQUEST_MANAGEMENT}?requestId=${fr.requestId}`, + ) + } + /> + ))} + + + + My Orders + + + {recentOrders.map((order) => ( + + navigate( + `${ROUTES.VOLUNTEER_ORDER_MANAGEMENT}?orderId=${order.orderId}`, + ) + } + /> + ))} + + + ); +}; + +export default VolunteerDashboard; diff --git a/apps/frontend/src/containers/volunteerManagement.tsx b/apps/frontend/src/containers/volunteerManagement.tsx index c48c92fd9..b89dc09ae 100644 --- a/apps/frontend/src/containers/volunteerManagement.tsx +++ b/apps/frontend/src/containers/volunteerManagement.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { ROUTES } from '../routes'; import { Table, Text, @@ -176,7 +177,7 @@ const VolunteerManagement: React.FC = () => { textStyle="p2" variant="underline" textDecorationColor="neutral.700" - href={`/pantry-management/${volunteer.id}`} + href={`${ROUTES.PANTRY_MANAGEMENT}/${volunteer.id}`} > View Assigned Pantries diff --git a/apps/frontend/src/containers/volunteerOrderManagement.tsx b/apps/frontend/src/containers/volunteerOrderManagement.tsx index 813f73cdb..10174c675 100644 --- a/apps/frontend/src/containers/volunteerOrderManagement.tsx +++ b/apps/frontend/src/containers/volunteerOrderManagement.tsx @@ -40,6 +40,7 @@ import OrderDetailsModal from '@components/forms/orderDetailsModal'; import CompleteRequiredActionsModal from '@components/forms/completeRequiredActionsModal'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; +import { useSearchParams } from 'react-router-dom'; type VolunteerOrderWithColor = VolunteerOrder & { assigneeColor?: string }; @@ -83,6 +84,7 @@ const VolunteerOrderManagement: React.FC = () => { const [alertState, setAlertMessage] = useAlert(); const [currentUser, setCurrentUser] = useState(null); + const [searchParams] = useSearchParams(); type FilterState = { selectedPantries: string[]; @@ -167,6 +169,16 @@ const VolunteerOrderManagement: React.FC = () => { fetchOrders(); }, [setAlertMessage]); + useEffect(() => { + const orderIdFromUrl = searchParams.get('orderId'); + + const allOrders = Object.values(statusOrders).flat(); + if (!orderIdFromUrl || allOrders.length === 0) return; + + const match = allOrders.find((o) => o.orderId === Number(orderIdFromUrl)); + if (match) setSelectedOrderId(match.orderId); + }, [searchParams, statusOrders]); + const resetPageForStatus = (status: OrderStatus) => { setCurrentPages((prev) => ({ ...prev, [status]: 1 })); }; diff --git a/apps/frontend/src/containers/volunteerRequestManagement.tsx b/apps/frontend/src/containers/volunteerRequestManagement.tsx index 4fad5f34d..67ec3451d 100644 --- a/apps/frontend/src/containers/volunteerRequestManagement.tsx +++ b/apps/frontend/src/containers/volunteerRequestManagement.tsx @@ -1,11 +1,19 @@ import React from 'react'; import ApiClient from '@api/apiClient'; import RequestManagement from '@components/foodRequestManagement'; +import { useSearchParams } from 'react-router-dom'; -const VolunteerRequestManagement: React.FC = () => ( - ApiClient.getVolunteerAssignedRequests()} - /> -); +const VolunteerRequestManagement: React.FC = () => { + const [searchParams] = useSearchParams(); + const requestIdParam = searchParams.get('requestId'); + const initialRequestId = requestIdParam ? Number(requestIdParam) : undefined; + + return ( + ApiClient.getVolunteerAssignedRequests()} + initialRequestId={initialRequestId} + /> + ); +}; export default VolunteerRequestManagement; diff --git a/apps/frontend/src/routes.ts b/apps/frontend/src/routes.ts index 976befab1..751837fe5 100644 --- a/apps/frontend/src/routes.ts +++ b/apps/frontend/src/routes.ts @@ -33,6 +33,8 @@ export const ROUTES = { PANTRY_ORDER_MANAGEMENT: '/pantry-order-management', REQUEST_FORM: '/request-form', + PANTRY_DASHBOARD: '/pantry-dashboard', + VOLUNTEER_DASHBOARD: '/volunteer-dashboard', FM_DONATION_MANAGEMENT: '/fm-donation-management', }; From 2eed8415711bc81fa3d546e1b50d3ffb94708121 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 3 May 2026 16:02:13 -0400 Subject: [PATCH 2/7] comments and dep updates to fix linters --- apps/backend/src/orders/order.controller.ts | 1 - apps/backend/src/orders/order.service.spec.ts | 30 +--------- apps/backend/src/orders/order.service.ts | 39 +++++++++---- .../src/pantries/pantries.controller.ts | 4 +- apps/backend/src/pantries/types.ts | 28 +++++++++ apps/frontend/src/api/apiClient.ts | 1 - .../src/components/foodRequestManagement.tsx | 29 +++++++--- .../frontend/src/components/signOutButton.tsx | 1 - .../src/containers/adminDonationStats.tsx | 6 +- .../src/containers/adminRequestManagement.tsx | 18 +++--- apps/frontend/src/containers/formRequests.tsx | 25 +++++--- .../src/containers/pantryDashboard.tsx | 57 +++++++++---------- .../src/containers/pantryOrderManagement.tsx | 25 +++++--- .../src/containers/volunteerDashboard.tsx | 43 +++++++------- .../containers/volunteerOrderManagement.tsx | 19 +++++-- .../containers/volunteerRequestManagement.tsx | 9 ++- apps/frontend/src/types/types.ts | 16 +++--- 17 files changed, 203 insertions(+), 148 deletions(-) diff --git a/apps/backend/src/orders/order.controller.ts b/apps/backend/src/orders/order.controller.ts index 134457722..972973f81 100644 --- a/apps/backend/src/orders/order.controller.ts +++ b/apps/backend/src/orders/order.controller.ts @@ -31,7 +31,6 @@ import { FilesInterceptor } from '@nestjs/platform-express'; import * as multer from 'multer'; import { ConfirmDeliveryDto } from './dtos/confirm-delivery.dto'; import { CompleteVolunteerActionDto } from './dtos/complete-volunteer-action.dto'; -import { FoodRequest } from '../foodRequests/request.entity'; import { CreateOrderDto } from './dtos/create-order.dto'; import { AuthenticatedRequest } from '../auth/authenticated-request'; import { Roles } from '../auth/roles.decorator'; diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 753c4f893..5132c23ab 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -408,44 +408,16 @@ describe('OrdersService', () => { const orders = await service.getOrdersByPantry(pantryId); expect(orders.length).toBe(2); - expect(orders.every((order) => order.request)).toBeDefined(); expect(orders.every((order) => order.request.pantryId === 1)).toBe(true); - expect(orders.every((order) => order.request.pantry)).toBeDefined(); - expect(orders.every((order) => order.assignee)).toBeDefined(); }); - it('returns empty list for pantry with no orderes', async () => { + it('returns empty list for pantry with no orders', async () => { const pantryId = 5; const orders = await service.getOrdersByPantry(pantryId); expect(orders).toEqual([]); }); - it('honors year filter (no results for future year)', async () => { - const pantryId = 1; - const orders = await service.getOrdersByPantry(pantryId, [2025]); - expect(orders).toEqual([]); - }); - - it('returns orders when a valid year filter is provided', async () => { - const pantryId = 1; - - // Change some order dates so we have 2024, 2025 and 2026 values - await testDataSource.query( - `UPDATE "orders" SET created_at='2025-01-01' WHERE order_id = 1`, - ); - await testDataSource.query( - `UPDATE "orders" SET created_at='2026-01-01' WHERE order_id = 2`, - ); - - const orders = await service.getOrdersByPantry(pantryId, [2024, 2025]); - expect(orders.length).toBeGreaterThan(0); - - const years = orders.map((o) => new Date(o.createdAt).getFullYear()); - expect(years).toContain(2025); - expect(years.every((y) => y === 2024 || y === 2025)).toBe(true); - }); - it('throws NotFoundException for non-existent pantry', async () => { const pantryId = 9999; diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index ff41610fa..51f74dfb8 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -24,6 +24,7 @@ import { DonationItemsService } from '../donationItems/donationItems.service'; import { AllocationsService } from '../allocations/allocations.service'; import { ApplicationStatus } from '../shared/types'; import { VolunteerOrder } from '../volunteers/types'; +import { OrderSummary } from '../pantries/types'; @Injectable() export class OrdersService { @@ -452,10 +453,7 @@ export class OrdersService { return updatedOrder; } - async getOrdersByPantry( - pantryId: number, - years?: number[], - ): Promise { + async getOrdersByPantry(pantryId: number): Promise { validateId(pantryId, 'Pantry'); const pantry = await this.pantryRepo.findOneBy({ pantryId }); @@ -468,18 +466,35 @@ export class OrdersService { .leftJoinAndSelect('order.request', 'request') .leftJoin('request.pantry', 'pantry') .addSelect('pantry.pantryName') - .leftJoinAndSelect('order.allocations', 'allocations') - .leftJoinAndSelect('allocations.item', 'item') .leftJoinAndSelect('order.assignee', 'assignee') .where('request.pantryId = :pantryId', { pantryId }); - if (years && years.length > 0) { - qb.andWhere('EXTRACT(YEAR FROM order.createdAt) IN (:...years)', { - years, - }); - } + const orders = await qb.getMany(); - return qb.getMany(); + return orders.map((order) => ({ + orderId: order.orderId, + status: order.status, + createdAt: order.createdAt.toISOString(), + shippedAt: order.shippedAt?.toISOString() ?? null, + deliveredAt: order.deliveredAt?.toISOString() ?? null, + request: { + pantryId: order.request.pantryId, + pantry: { + pantryName: order.request.pantry.pantryName, + volunteers: + order.request.pantry.volunteers?.map((v) => ({ + id: v.id, + firstName: v.firstName, + lastName: v.lastName, + })) ?? null, + }, + }, + assignee: { + id: order.assignee.id, + firstName: order.assignee.firstName, + lastName: order.assignee.lastName, + }, + })); } async updateTrackingCostInfo(orderId: number, dto: TrackingCostDto) { diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index e7c886a18..b8286b0d4 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -27,8 +27,8 @@ import { ServeAllergicChildren, ApprovedPantryResponse, TotalStats, + OrderSummary, } from './types'; -import { Order } from '../orders/order.entity'; import { OrdersService } from '../orders/order.service'; import { CheckOwnership, pipeNullable } from '../auth/ownership.decorator'; import { Public } from '../auth/public.decorator'; @@ -122,7 +122,7 @@ export class PantriesController { @Get('/:pantryId/orders') async getOrders( @Param('pantryId', ParseIntPipe) pantryId: number, - ): Promise { + ): Promise { return this.ordersService.getOrdersByPantry(pantryId); } diff --git a/apps/backend/src/pantries/types.ts b/apps/backend/src/pantries/types.ts index bcc9e4ab8..8b904d331 100644 --- a/apps/backend/src/pantries/types.ts +++ b/apps/backend/src/pantries/types.ts @@ -1,3 +1,5 @@ +import { OrderStatus } from '../orders/types'; + export interface ApprovedPantryResponse { pantryId: number; pantryName: string; @@ -5,6 +7,32 @@ export interface ApprovedPantryResponse { volunteers: AssignedVolunteer[]; } +export interface OrderSummary { + orderId: number; + status: OrderStatus; + createdAt: string; + shippedAt: string | null; + deliveredAt: string | null; + request: { + pantryId: number; + pantry: { + pantryName: string; + volunteers: + | { + id: number; + firstName: string; + lastName: string; + }[] + | null; + }; + }; + assignee: { + id: number; + firstName: string; + lastName: string; + }; +} + export interface AssignedVolunteer { userId: number; firstName: string; diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index c6941ee1a..a3cd19c09 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -25,7 +25,6 @@ import { ConfirmDeliveryDto, OrderWithoutRelations, FoodRequestSummaryDto, - OrderWithoutFoodManufacturer, PantryWithUser, Assignments, PantryStats, diff --git a/apps/frontend/src/components/foodRequestManagement.tsx b/apps/frontend/src/components/foodRequestManagement.tsx index 169b16168..971bc6d09 100644 --- a/apps/frontend/src/components/foodRequestManagement.tsx +++ b/apps/frontend/src/components/foodRequestManagement.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Box, Button, @@ -20,6 +20,8 @@ import VolunteerCloseRequestActionModal from '@components/forms/volunteerCloseRe import VolunteerRequestActionRequiredModal from '@components/forms/volunteerRequestActionRequiredModal'; import CreateNewOrderModal from '@components/forms/createNewOrderModal'; import { useAlert } from '../hooks/alert'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { ROUTES } from '../routes'; interface RequestManagementProps { fetchRequests: () => Promise; @@ -52,7 +54,10 @@ const RequestManagement: React.FC = ({ const [alertState, setAlertMessage] = useAlert(); const [isAlertError, setIsAlertError] = useState(true); - const loadRequests = async () => { + const navigate = useNavigate(); + const location = useLocation(); + + const loadRequests = useCallback(async () => { try { const data = await fetchData(); setRequests(data); @@ -60,11 +65,11 @@ const RequestManagement: React.FC = ({ setIsAlertError(true); setAlertMessage('Error fetching requests'); } - }; + }, [fetchData, setAlertMessage]); useEffect(() => { loadRequests(); - }, []); + }, [loadRequests]); useEffect(() => { setCurrentPage(1); @@ -73,8 +78,13 @@ const RequestManagement: React.FC = ({ useEffect(() => { if (!initialRequestId || requests.length === 0) return; const match = requests.find((r) => r.requestId === initialRequestId); - if (match) setSelectedViewDetailsRequest(match); - }, [initialRequestId, requests]); + + if (match) { + setSelectedViewDetailsRequest(match); + } else { + navigate(ROUTES.REQUEST_FORM, { replace: true }); + } + }, [initialRequestId, requests, navigate]); const pantryOptions = [ ...new Set( @@ -369,7 +379,12 @@ const RequestManagement: React.FC = ({ setSelectedViewDetailsRequest(null)} + onClose={() => { + setSelectedViewDetailsRequest(null); + if (initialRequestId) { + navigate(location.pathname, { replace: true }); + } + }} /> )} diff --git a/apps/frontend/src/components/signOutButton.tsx b/apps/frontend/src/components/signOutButton.tsx index 2de170fe2..cff94f15c 100644 --- a/apps/frontend/src/components/signOutButton.tsx +++ b/apps/frontend/src/components/signOutButton.tsx @@ -1,4 +1,3 @@ -import apiClient from '@api/apiClient'; import { Button, ButtonProps } from '@chakra-ui/react'; import { signOut } from 'aws-amplify/auth'; import { useNavigate } from 'react-router-dom'; diff --git a/apps/frontend/src/containers/adminDonationStats.tsx b/apps/frontend/src/containers/adminDonationStats.tsx index c699c5110..22f1d5c93 100644 --- a/apps/frontend/src/containers/adminDonationStats.tsx +++ b/apps/frontend/src/containers/adminDonationStats.tsx @@ -57,7 +57,7 @@ const AdminDonationStats: React.FC = () => { } }; fetchInitialData(); - }, []); + }, [setAlertMessage]); useEffect(() => { // Total stats only displayed on first page, so no need to do anything on page change @@ -74,7 +74,7 @@ const AdminDonationStats: React.FC = () => { } }; fetchTotalStats(); - }, [selectedYears, currentPage]); + }, [setAlertMessage, selectedYears, currentPage]); useEffect(() => { const fetchStats = async () => { @@ -90,7 +90,7 @@ const AdminDonationStats: React.FC = () => { } }; fetchStats(); - }, [selectedPantries, selectedYears, currentPage]); + }, [setAlertMessage, selectedPantries, selectedYears, currentPage]); const handlePantryNameFilterChange = (name: string, checked: boolean) => { // For simplicity, reset the page diff --git a/apps/frontend/src/containers/adminRequestManagement.tsx b/apps/frontend/src/containers/adminRequestManagement.tsx index 6a85dccc2..af4e2d3e0 100644 --- a/apps/frontend/src/containers/adminRequestManagement.tsx +++ b/apps/frontend/src/containers/adminRequestManagement.tsx @@ -1,12 +1,16 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import ApiClient from '@api/apiClient'; import RequestManagement from '@components/foodRequestManagement'; -const AdminRequestManagement: React.FC = () => ( - ApiClient.getAllFoodRequests()} - enableVolunteerActions={false} - /> -); +const AdminRequestManagement: React.FC = () => { + const fetchRequests = useCallback(() => ApiClient.getAllFoodRequests(), []); + + return ( + + ); +}; export default AdminRequestManagement; diff --git a/apps/frontend/src/containers/formRequests.tsx b/apps/frontend/src/containers/formRequests.tsx index d9d21251c..5dc980bc7 100644 --- a/apps/frontend/src/containers/formRequests.tsx +++ b/apps/frontend/src/containers/formRequests.tsx @@ -15,17 +15,14 @@ import { } from '@chakra-ui/react'; import { ChevronRight, ChevronLeft } from 'lucide-react'; import FoodRequestFormModal from '@components/forms/requestFormModal'; -import { - FoodRequest, - FoodRequestStatus, - FoodRequestSummaryDto, -} from '../types/types'; +import { FoodRequestStatus, FoodRequestSummaryDto } from '../types/types'; import RequestDetailsModal from '@components/forms/requestDetailsModal'; import { formatDate } from '@utils/utils'; import ApiClient from '@api/apiClient'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; -import { useSearchParams } from 'react-router-dom'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { ROUTES } from '../routes'; const FormRequests: React.FC = () => { const [currentPage, setCurrentPage] = useState(1); @@ -42,6 +39,7 @@ const FormRequests: React.FC = () => { useState(null); const [searchParams] = useSearchParams(); + const navigate = useNavigate(); const [alertState, setAlertMessage] = useAlert(); const pageSize = 10; @@ -78,8 +76,12 @@ const FormRequests: React.FC = () => { const match = requests.find( (r) => r.requestId === Number(requestIdFromUrl), ); - if (match) setOpenReadOnlyRequest(match); - }, [searchParams, requests]); + if (match) { + setOpenReadOnlyRequest(match); + } else { + navigate(ROUTES.REQUEST_FORM, { replace: true }); + } + }, [searchParams, requests, navigate]); const paginatedRequests = requests.slice( (currentPage - 1) * pageSize, @@ -223,7 +225,12 @@ const FormRequests: React.FC = () => { setOpenReadOnlyRequest(null)} + onClose={() => { + setOpenReadOnlyRequest(null); + if (searchParams.get('requestId')) { + navigate(ROUTES.REQUEST_FORM, { replace: true }); + } + }} /> )} diff --git a/apps/frontend/src/containers/pantryDashboard.tsx b/apps/frontend/src/containers/pantryDashboard.tsx index 56cec6e0d..e3ff30545 100644 --- a/apps/frontend/src/containers/pantryDashboard.tsx +++ b/apps/frontend/src/containers/pantryDashboard.tsx @@ -23,45 +23,40 @@ const PantryDashboard: React.FC = () => { >([]); const [recentOrders, setRecentOrders] = useState([]); - const fetchRecentFoodRequests = async (pantryId: number) => { - try { - const pantryFoodRequests = await ApiClient.getPantryRequests(pantryId); - const sortedFoodRequests = pantryFoodRequests.sort( - (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => - new Date(b.requestedAt).getTime() - new Date(a.requestedAt).getTime(), - ); - setRecentFoodRequests(sortedFoodRequests.slice(0, 2)); - } catch { - setAlertMessage('Error fetching pantry food requests'); - } - }; - - const fetchRecentOrders = async (pantryId: number) => { - try { - const pantryOrders = await ApiClient.getPantryOrders(pantryId); - const sortedOrders = pantryOrders.sort( - (a: OrderSummary, b: OrderSummary) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ); - setRecentOrders(sortedOrders.slice(0, 4)); - } catch { - setAlertMessage('Error fetching orders'); - } - }; - useEffect(() => { const fetchDashboardData = async () => { - let pantryData: PantryWithUser; + let pantryId: number; try { - const pantryId = await ApiClient.getCurrentUserPantryId(); - pantryData = await ApiClient.getPantry(pantryId); + pantryId = await ApiClient.getCurrentUserPantryId(); + const pantryData = await ApiClient.getPantry(pantryId); setPantry(pantryData); } catch { setAlertMessage('Error fetching pantry information'); return; } - fetchRecentFoodRequests(pantryData.pantryId); - fetchRecentOrders(pantryData.pantryId); + + try { + const pantryFoodRequests = await ApiClient.getPantryRequests(pantryId); + const sortedFoodRequests = pantryFoodRequests.sort( + (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => + new Date(b.requestedAt).getTime() - + new Date(a.requestedAt).getTime(), + ); + setRecentFoodRequests(sortedFoodRequests.slice(0, 2)); + } catch { + setAlertMessage('Error fetching pantry food requests'); + } + + try { + const pantryOrders = await ApiClient.getPantryOrders(pantryId); + const sortedOrders = pantryOrders.sort( + (a: OrderSummary, b: OrderSummary) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + setRecentOrders(sortedOrders.slice(0, 4)); + } catch { + setAlertMessage('Error fetching orders'); + } }; fetchDashboardData(); }, [setAlertMessage]); diff --git a/apps/frontend/src/containers/pantryOrderManagement.tsx b/apps/frontend/src/containers/pantryOrderManagement.tsx index 94118e1f2..6d2bc2fdd 100644 --- a/apps/frontend/src/containers/pantryOrderManagement.tsx +++ b/apps/frontend/src/containers/pantryOrderManagement.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Box, Button, @@ -23,7 +23,8 @@ import OrderReceivedActionModal from '@components/forms/orderReceivedActionModal import OrderDetailsModal from '@components/forms/orderDetailsModal'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; -import { useSearchParams } from 'react-router-dom'; +import { useSearchParams, useNavigate } from 'react-router-dom'; +import { ROUTES } from '../routes'; type OrderWithColor = OrderSummary & { assigneeColor?: string }; const MAX_PER_STATUS = 5; @@ -57,6 +58,7 @@ const PantryOrderManagement: React.FC = () => { const [alertState, setAlertMessage] = useAlert(); const [searchParams] = useSearchParams(); + const navigate = useNavigate(); // State to hold filter state per status type FilterState = { @@ -80,7 +82,7 @@ const PantryOrderManagement: React.FC = () => { }, }); - const fetchOrders = async () => { + const fetchOrders = useCallback(async () => { try { const pantryId = await ApiClient.getCurrentUserPantryId(); const data = await ApiClient.getPantryOrders(pantryId); @@ -112,11 +114,11 @@ const PantryOrderManagement: React.FC = () => { setIsAlertError(true); setAlertMessage('Failed to fetch orders'); } - }; + }, [setAlertMessage]); useEffect(() => { fetchOrders(); - }, []); + }, [fetchOrders]); useEffect(() => { const orderIdFromUrl = searchParams.get('orderId'); @@ -124,8 +126,12 @@ const PantryOrderManagement: React.FC = () => { if (!orderIdFromUrl || allOrders.length === 0) return; const match = allOrders.find((o) => o.orderId === Number(orderIdFromUrl)); - if (match) setSelectedOrderId(match.orderId); - }, [searchParams, statusOrders]); + if (match) { + setSelectedOrderId(match.orderId); + } else { + navigate(ROUTES.PANTRY_ORDER_MANAGEMENT, { replace: true }); + } + }, [searchParams, statusOrders, navigate]); // Helper to reset page for a specific status const resetPageForStatus = (status: OrderStatus) => { @@ -201,7 +207,10 @@ const PantryOrderManagement: React.FC = () => { setSelectedOrderId(null)} + onClose={() => { + setSelectedOrderId(null); + navigate(ROUTES.PANTRY_ORDER_MANAGEMENT, { replace: true }); + }} /> )} diff --git a/apps/frontend/src/containers/volunteerDashboard.tsx b/apps/frontend/src/containers/volunteerDashboard.tsx index 2075c19e3..c33843670 100644 --- a/apps/frontend/src/containers/volunteerDashboard.tsx +++ b/apps/frontend/src/containers/volunteerDashboard.tsx @@ -19,28 +19,6 @@ const VolunteerDashboard: React.FC = () => { >([]); const [recentOrders, setRecentOrders] = useState([]); - const fetchRecentFoodRequests = async () => { - try { - const requests = await ApiClient.getVolunteerAssignedRequests(); - const sorted = requests.sort( - (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => - new Date(b.requestedAt).getTime() - new Date(a.requestedAt).getTime(), - ); - setRecentFoodRequests(sorted.slice(0, 2)); - } catch { - setAlertMessage('Error fetching food requests'); - } - }; - - const fetchRecentOrders = async () => { - try { - const orders = await ApiClient.getVolunteerRecentOrders(); - setRecentOrders(orders); - } catch { - setAlertMessage('Error fetching orders'); - } - }; - useEffect(() => { const fetchDashboardData = async () => { try { @@ -50,8 +28,25 @@ const VolunteerDashboard: React.FC = () => { setAlertMessage('Error fetching user information'); return; } - fetchRecentFoodRequests(); - fetchRecentOrders(); + + try { + const requests = await ApiClient.getVolunteerAssignedRequests(); + const sorted = requests.sort( + (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => + new Date(b.requestedAt).getTime() - + new Date(a.requestedAt).getTime(), + ); + setRecentFoodRequests(sorted.slice(0, 2)); + } catch { + setAlertMessage('Error fetching food requests'); + } + + try { + const orders = await ApiClient.getVolunteerRecentOrders(); + setRecentOrders(orders); + } catch { + setAlertMessage('Error fetching orders'); + } }; fetchDashboardData(); }, [setAlertMessage]); diff --git a/apps/frontend/src/containers/volunteerOrderManagement.tsx b/apps/frontend/src/containers/volunteerOrderManagement.tsx index 10174c675..8d416dfcd 100644 --- a/apps/frontend/src/containers/volunteerOrderManagement.tsx +++ b/apps/frontend/src/containers/volunteerOrderManagement.tsx @@ -40,7 +40,8 @@ import OrderDetailsModal from '@components/forms/orderDetailsModal'; import CompleteRequiredActionsModal from '@components/forms/completeRequiredActionsModal'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; -import { useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { ROUTES } from '../routes'; type VolunteerOrderWithColor = VolunteerOrder & { assigneeColor?: string }; @@ -85,6 +86,7 @@ const VolunteerOrderManagement: React.FC = () => { const [alertState, setAlertMessage] = useAlert(); const [currentUser, setCurrentUser] = useState(null); const [searchParams] = useSearchParams(); + const navigate = useNavigate(); type FilterState = { selectedPantries: string[]; @@ -176,8 +178,12 @@ const VolunteerOrderManagement: React.FC = () => { if (!orderIdFromUrl || allOrders.length === 0) return; const match = allOrders.find((o) => o.orderId === Number(orderIdFromUrl)); - if (match) setSelectedOrderId(match.orderId); - }, [searchParams, statusOrders]); + if (match) { + setSelectedOrderId(match.orderId); + } else { + navigate(ROUTES.VOLUNTEER_ORDER_MANAGEMENT, { replace: true }); + } + }, [searchParams, statusOrders, navigate]); const resetPageForStatus = (status: OrderStatus) => { setCurrentPages((prev) => ({ ...prev, [status]: 1 })); @@ -357,6 +363,8 @@ const OrderStatusSection: React.FC = ({ const [isFilterOpen, setIsFilterOpen] = useState(false); const [isSortOpen, setIsSortOpen] = useState(false); + const navigate = useNavigate(); + const MAX_PER_STATUS = 5; const totalPages = Math.ceil(totalOrders / MAX_PER_STATUS); @@ -804,7 +812,10 @@ const OrderStatusSection: React.FC = ({ onOrderSelect(null)} + onClose={() => { + onOrderSelect(null); + navigate(ROUTES.VOLUNTEER_ORDER_MANAGEMENT, { replace: true }); + }} /> )} diff --git a/apps/frontend/src/containers/volunteerRequestManagement.tsx b/apps/frontend/src/containers/volunteerRequestManagement.tsx index 67ec3451d..dd6c4fb31 100644 --- a/apps/frontend/src/containers/volunteerRequestManagement.tsx +++ b/apps/frontend/src/containers/volunteerRequestManagement.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import ApiClient from '@api/apiClient'; import RequestManagement from '@components/foodRequestManagement'; import { useSearchParams } from 'react-router-dom'; @@ -8,9 +8,14 @@ const VolunteerRequestManagement: React.FC = () => { const requestIdParam = searchParams.get('requestId'); const initialRequestId = requestIdParam ? Number(requestIdParam) : undefined; + const fetchRequests = useCallback( + () => ApiClient.getVolunteerAssignedRequests(), + [], + ); + return ( ApiClient.getVolunteerAssignedRequests()} + fetchRequests={fetchRequests} initialRequestId={initialRequestId} /> ); diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 20318879c..6f71f6322 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -492,17 +492,19 @@ export interface OrderSummary { orderId: number; status: OrderStatus; createdAt: string; - shippedAt?: string; - deliveredAt?: string; + shippedAt: string | null; + deliveredAt: string | null; request: { pantryId: number; pantry: { pantryName: string; - volunteers?: { - id: number; - firstName: string; - lastName: string; - }[]; + volunteers: + | { + id: number; + firstName: string; + lastName: string; + }[] + | null; }; }; assignee: { From 9d5ccd214cf3008421ab9b5c568e63c1e65f46dc Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 3 May 2026 16:10:02 -0400 Subject: [PATCH 3/7] fixed tests --- apps/backend/src/pantries/pantries.controller.spec.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index 5a7233a3c..3acdddc03 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -16,6 +16,7 @@ import { ServeAllergicChildren, ApprovedPantryResponse, TotalStats, + OrderSummary, } from './types'; import { EmailsService } from '../emails/email.service'; import { ApplicationStatus } from '../shared/types'; @@ -359,21 +360,17 @@ describe('PantriesController', () => { it('should return orders for a pantry', async () => { const pantryId = 24; - const mockOrders: Partial[] = [ + const mockOrders: Partial[] = [ { orderId: 26, - requestId: 26, - foodManufacturerId: 32, }, { orderId: 27, - requestId: 27, - foodManufacturerId: 33, }, ]; mockOrdersService.getOrdersByPantry.mockResolvedValue( - mockOrders as Order[], + mockOrders as OrderSummary[], ); const result = await controller.getOrders(pantryId); From ee5efbd682789dba2993964cd8aefcd935df3b5c Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sat, 16 May 2026 09:04:07 -0700 Subject: [PATCH 4/7] Comments --- apps/frontend/src/containers/homepage.tsx | 2 +- apps/frontend/src/containers/pantryDashboard.tsx | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index 79ac37e7d..fe7978d5b 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -36,7 +36,7 @@ const Homepage: React.FC = () => { - Dasboard + Dashboard diff --git a/apps/frontend/src/containers/pantryDashboard.tsx b/apps/frontend/src/containers/pantryDashboard.tsx index e3ff30545..182c9a9ef 100644 --- a/apps/frontend/src/containers/pantryDashboard.tsx +++ b/apps/frontend/src/containers/pantryDashboard.tsx @@ -61,7 +61,7 @@ const PantryDashboard: React.FC = () => { fetchDashboardData(); }, [setAlertMessage]); - if (!pantry) return; + if (!pantry) return null; return ( @@ -83,6 +83,7 @@ const PantryDashboard: React.FC = () => { {recentFoodRequests.map((fr) => ( { {recentOrders.map((order) => ( Date: Sat, 16 May 2026 09:43:14 -0700 Subject: [PATCH 5/7] comments --- apps/backend/src/volunteers/volunteers.controller.spec.ts | 2 +- apps/backend/src/volunteers/volunteers.controller.ts | 2 +- apps/frontend/src/api/apiClient.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts index 1691952d7..c492d02b3 100644 --- a/apps/backend/src/volunteers/volunteers.controller.spec.ts +++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts @@ -214,7 +214,7 @@ describe('VolunteersController', () => { }); }); - describe('GET /me/my-recent-orders', () => { + describe('GET /me/recent-orders', () => { it('returns the 2 most recent orders for a volunteer', async () => { const req: AuthenticatedRequest = { user: { id: 6 }, diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index 5dc36a729..7e6c08f08 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -62,7 +62,7 @@ export class VolunteersController { } @Roles(Role.VOLUNTEER) - @Get('/me/my-recent-orders') + @Get('/me/recent-orders') async getRecentOrders( @Req() req: AuthenticatedRequest, ): Promise { diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index a3cd19c09..544a4225e 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -255,7 +255,7 @@ export class ApiClient { public async getVolunteerRecentOrders(): Promise { return this.axiosInstance - .get(`/api/volunteers/me/my-recent-orders`) + .get(`/api/volunteers/me/recent-orders`) .then((response) => response.data); } From 793503f0b1ff1e8d2ccfa0c6c29e4e7c78df2c89 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sat, 16 May 2026 22:24:50 -0700 Subject: [PATCH 6/7] Resolved conflicts --- apps/frontend/src/app.tsx | 2 +- apps/frontend/src/components/Navbar.tsx | 2 +- apps/frontend/src/containers/homepage.tsx | 2 +- apps/frontend/src/containers/volunteerManagement.tsx | 2 +- apps/frontend/src/routes.ts | 3 +-- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index d14a1f58b..c7735e1d4 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -241,7 +241,7 @@ const router = createBrowserRouter([ ), }, { - path: ROUTES.ADMIN_PANTRY_MANAGEMENT, + path: ROUTES.PANTRY_MANAGEMENT, element: ( diff --git a/apps/frontend/src/components/Navbar.tsx b/apps/frontend/src/components/Navbar.tsx index 237c5b066..ef77f564a 100644 --- a/apps/frontend/src/components/Navbar.tsx +++ b/apps/frontend/src/components/Navbar.tsx @@ -38,7 +38,7 @@ const ROLE_NAV_SECTIONS: Record = { type: 'group', label: 'Pantries', children: [ - { label: 'Pantry Management', to: ROUTES.ADMIN_PANTRY_MANAGEMENT }, + { label: 'Pantry Management', to: ROUTES.PANTRY_MANAGEMENT }, { label: 'Application Review', to: ROUTES.APPROVE_PANTRIES }, ], }, diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index 12eabcd37..f678e60fa 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -168,7 +168,7 @@ const Homepage: React.FC = () => { - + Pantry Management diff --git a/apps/frontend/src/containers/volunteerManagement.tsx b/apps/frontend/src/containers/volunteerManagement.tsx index 7ab7ac8d1..54707fe94 100644 --- a/apps/frontend/src/containers/volunteerManagement.tsx +++ b/apps/frontend/src/containers/volunteerManagement.tsx @@ -181,7 +181,7 @@ const VolunteerManagement: React.FC = () => { textStyle="p2" variant="underline" textDecorationColor="neutral.700" - href={`${ROUTES.ADMIN_PANTRY_MANAGEMENT}/${volunteer.id}`} + href={`${ROUTES.PANTRY_MANAGEMENT}/${volunteer.id}`} > View Assigned Pantries diff --git a/apps/frontend/src/routes.ts b/apps/frontend/src/routes.ts index d360092ff..3ea11f32c 100644 --- a/apps/frontend/src/routes.ts +++ b/apps/frontend/src/routes.ts @@ -20,12 +20,11 @@ export const ROUTES = { APPROVE_PANTRIES: '/approve-pantries', APPROVE_FOOD_MANUFACTURERS: '/approve-food-manufacturers', VOLUNTEER_MANAGEMENT: '/volunteer-management', - + PANTRY_MANAGEMENT: '/pantry-management', ADMIN_ORDER_MANAGEMENT: '/admin-order-management', ADMIN_DONATION: '/admin-donation', ADMIN_DONATION_STATS: '/admin-donation-stats', ADMIN_REQUEST_MANAGEMENT: '/admin-request-management', - ADMIN_PANTRY_MANAGEMENT: '/admin-pantry-management', ADMIN_DASHBOARD: '/admin-dashboard', VOLUNTEER_ASSIGNED_PANTRIES: '/volunteer-assigned-pantries', From 58a449652e617ec0e2f41dbcf32df5cd02e506ee Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sat, 16 May 2026 22:33:00 -0700 Subject: [PATCH 7/7] Updated dashboard links --- apps/frontend/src/components/Navbar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/frontend/src/components/Navbar.tsx b/apps/frontend/src/components/Navbar.tsx index ef77f564a..0afea73fb 100644 --- a/apps/frontend/src/components/Navbar.tsx +++ b/apps/frontend/src/components/Navbar.tsx @@ -272,8 +272,8 @@ const Navbar: React.FC = () => { // Should be changed once other dashboards are implmented const ROLE_DASHBOARD_ROUTE: Record = { [Role.ADMIN]: ROUTES.ADMIN_DASHBOARD, - [Role.VOLUNTEER]: ROUTES.HOME, - [Role.PANTRY]: ROUTES.HOME, + [Role.VOLUNTEER]: ROUTES.VOLUNTEER_DASHBOARD, + [Role.PANTRY]: ROUTES.PANTRY_DASHBOARD, [Role.FOODMANUFACTURER]: ROUTES.HOME, };