From 2b20d7ecea287fcaad2439206a16c03d4779916e Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Wed, 22 Jul 2026 22:50:58 -0700 Subject: [PATCH 1/4] Initial volunteer bugbash --- apps/backend/src/orders/order.service.ts | 4 + ...unteer-stats.dto.ts => admin-stats.dto.ts} | 2 +- apps/backend/src/users/users.controller.ts | 7 +- apps/backend/src/users/users.service.spec.ts | 31 +- apps/backend/src/users/users.service.ts | 22 +- .../volunteers/dtos/volunteer-stats.dto.ts | 5 + .../src/volunteers/volunteers.module.ts | 3 +- .../src/volunteers/volunteers.service.spec.ts | 126 ++++ .../src/volunteers/volunteers.service.ts | 81 ++- .../src/components/foodRequestManagement.tsx | 130 ++-- .../components/forms/orderDetailsModal.tsx | 558 +++++++++--------- .../components/forms/resetPasswordModal.tsx | 55 +- 12 files changed, 644 insertions(+), 380 deletions(-) rename apps/backend/src/users/dtos/{admin-volunteer-stats.dto.ts => admin-stats.dto.ts} (71%) create mode 100644 apps/backend/src/volunteers/dtos/volunteer-stats.dto.ts diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 4ba73af73..a0c15ccd1 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -1,5 +1,7 @@ import { BadRequestException, + forwardRef, + Inject, Injectable, InternalServerErrorException, Logger, @@ -47,7 +49,9 @@ export class OrdersService { @InjectRepository(Pantry) private pantryRepo: Repository, @InjectRepository(Donation) private donationRepo: Repository, @InjectRepository(FoodRequest) private requestRepo: Repository, + @Inject(forwardRef(() => RequestsService)) private requestsService: RequestsService, + @Inject(forwardRef(() => UsersService)) private usersService: UsersService, private manufacturerService: FoodManufacturersService, private donationItemsService: DonationItemsService, diff --git a/apps/backend/src/users/dtos/admin-volunteer-stats.dto.ts b/apps/backend/src/users/dtos/admin-stats.dto.ts similarity index 71% rename from apps/backend/src/users/dtos/admin-volunteer-stats.dto.ts rename to apps/backend/src/users/dtos/admin-stats.dto.ts index 7fd013789..fec82c6de 100644 --- a/apps/backend/src/users/dtos/admin-volunteer-stats.dto.ts +++ b/apps/backend/src/users/dtos/admin-stats.dto.ts @@ -1,4 +1,4 @@ -export type AdminVolunteerStats = { +export type AdminStatsDto = { 'Food Requests': string; Orders: string; Donations: string; diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index 3fc193229..0c0e6d396 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -14,11 +14,12 @@ import { userSchemaDto } from './dtos/userSchema.dto'; import { UpdateUserInfoDto } from './dtos/update-user-info.dto'; import { PendingApplication, Role } from './types'; import { AuthenticatedRequest } from '../auth/authenticated-request'; -import { AdminVolunteerStats } from './dtos/admin-volunteer-stats.dto'; +import { AdminStatsDto } from './dtos/admin-stats.dto'; import { PantryStatsDto } from '../pantries/dtos/pantry-stats.dto'; import { ManufacturerStatsDto } from '../foodManufacturers/dtos/manufacturer-stats.dto'; import { Roles } from '../auth/roles.decorator'; import { CheckOwnership, OwnerIdResolver } from '../auth/ownership.decorator'; +import { VolunteerStatsDto } from '../volunteers/dtos/volunteer-stats.dto'; const resolveUserAuthorizedUserIds: OwnerIdResolver = async ({ entityId }) => [ entityId, @@ -40,7 +41,9 @@ export class UsersController { @Get('/:id/stats') async getUserDashboardStats( @Param('id', ParseIntPipe) userId: number, - ): Promise { + ): Promise< + AdminStatsDto | PantryStatsDto | ManufacturerStatsDto | VolunteerStatsDto + > { return this.usersService.getUserDashboardStats(userId); } diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 7694e1526..311820bfb 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -32,6 +32,7 @@ import { FoodManufacturersService } from '../foodManufacturers/manufacturers.ser import { DonationItemsService } from '../donationItems/donationItems.service'; import { AllocationsService } from '../allocations/allocations.service'; import { PantriesService } from '../pantries/pantries.service'; +import { VolunteersService } from '../volunteers/volunteers.service'; jest.setTimeout(60000); @@ -50,6 +51,7 @@ describe('UsersService', () => { let donationService: DonationService; let pantriesService: PantriesService; let foodManufacturersService: FoodManufacturersService; + let volunteersService: VolunteersService; beforeAll(async () => { process.env.SEND_AUTOMATED_EMAILS = 'true'; @@ -64,6 +66,7 @@ describe('UsersService', () => { const module: TestingModule = await Test.createTestingModule({ providers: [ UsersService, + VolunteersService, RequestsService, OrdersService, DonationService, @@ -125,6 +128,7 @@ describe('UsersService', () => { foodManufacturersService = module.get( FoodManufacturersService, ); + volunteersService = module.get(VolunteersService); }); beforeEach(async () => { @@ -509,7 +513,7 @@ describe('UsersService', () => { }); }); - describe('getAdminVolunteerMonthlyAggregatedStats', () => { + describe('getAdminMonthlyAggregatedStats', () => { it('should return correct aggregated counts for the current month', async () => { const foodRequestRepo = testDataSource.getRepository(FoodRequest); @@ -543,7 +547,7 @@ describe('UsersService', () => { ); await foodRequestRepo.save(existingRequest); - const stats = await service.getAdminVolunteerMonthlyAggregatedStats(); + const stats = await service.getAdminMonthlyAggregatedStats(); const expectedKeys = [ 'Food Requests', @@ -586,7 +590,7 @@ describe('UsersService', () => { existingRequest2.requestedAt = startOfMonth; await foodRequestRepo.save(existingRequest2); - const stats = await service.getAdminVolunteerMonthlyAggregatedStats(); + const stats = await service.getAdminMonthlyAggregatedStats(); const expectedKeys = [ 'Food Requests', @@ -610,7 +614,7 @@ describe('UsersService', () => { }); it('should return just volunteer count if no other fields are relative to current month', async () => { - const stats = await service.getAdminVolunteerMonthlyAggregatedStats(); + const stats = await service.getAdminMonthlyAggregatedStats(); const expectedKeys = [ 'Food Requests', @@ -653,7 +657,7 @@ describe('UsersService', () => { existingRequest2.requestedAt = startOfCurrentMonth; await foodRequestRepo.save(existingRequest2); - const stats = await service.getAdminVolunteerMonthlyAggregatedStats(); + const stats = await service.getAdminMonthlyAggregatedStats(); const expectedKeys = [ 'Food Requests', @@ -691,11 +695,8 @@ describe('UsersService', () => { }); describe('getUserDashboardStats', () => { - it('should call getAdminVolunteerMonthlyAggregatedStats for admin user', async () => { - const spy = jest.spyOn( - service, - 'getAdminVolunteerMonthlyAggregatedStats', - ); + it('should call getAdminMonthlyAggregatedStats for admin user', async () => { + const spy = jest.spyOn(service, 'getAdminMonthlyAggregatedStats'); const result = await service.getUserDashboardStats(1); @@ -708,21 +709,17 @@ describe('UsersService', () => { ]); }); - it('should call getAdminVolunteerMonthlyAggregatedStats for volunteer user', async () => { + it('should call getVolunteerDashboardStats for volunteer user', async () => { // Maria Garcia (id=7) is a volunteer - const spy = jest.spyOn( - service, - 'getAdminVolunteerMonthlyAggregatedStats', - ); + const spy = jest.spyOn(volunteersService, 'getVolunteerDashboardStats'); const result = await service.getUserDashboardStats(7); - expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith(7); expect(Object.keys(result)).toEqual([ 'Food Requests', 'Orders', 'Donations', - 'Volunteers', ]); }); diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 70fddc315..4c4c42159 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -6,6 +6,7 @@ import { InternalServerErrorException, NotFoundException, } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { Between, DataSource, In, Repository } from 'typeorm'; import { User } from './users.entity'; @@ -23,10 +24,12 @@ import { PantryStatsDto } from '../pantries/dtos/pantry-stats.dto'; import { ManufacturerStatsDto } from '../foodManufacturers/dtos/manufacturer-stats.dto'; import { PantriesService } from '../pantries/pantries.service'; import { FoodManufacturersService } from '../foodManufacturers/manufacturers.service'; -import { AdminVolunteerStats } from './dtos/admin-volunteer-stats.dto'; +import { AdminStatsDto } from './dtos/admin-stats.dto'; import { Pantry } from '../pantries/pantries.entity'; import { FoodManufacturer } from '../foodManufacturers/manufacturers.entity'; import { ApplicationStatus } from '../shared/types'; +import { VolunteerStatsDto } from '../volunteers/dtos/volunteer-stats.dto'; +import { VolunteersService } from '../volunteers/volunteers.service'; @Injectable() export class UsersService { @@ -51,6 +54,7 @@ export class UsersService { private pantriesService: PantriesService, @Inject(forwardRef(() => FoodManufacturersService)) private foodManufacturersService: FoodManufacturersService, + private moduleRef: ModuleRef, ) {} async create(createUserDto: userSchemaDto): Promise { @@ -268,7 +272,7 @@ export class UsersService { return user; } - async getAdminVolunteerMonthlyAggregatedStats(): Promise { + async getAdminMonthlyAggregatedStats(): Promise { const now = new Date(); const startMonth = new Date(now.getFullYear(), now.getMonth(), 1); const endMonth = new Date( @@ -305,11 +309,19 @@ export class UsersService { async getUserDashboardStats( userId: number, - ): Promise { + ): Promise< + AdminStatsDto | PantryStatsDto | ManufacturerStatsDto | VolunteerStatsDto + > { const user = await this.findOne(userId); - if (user.role === Role.ADMIN || user.role === Role.VOLUNTEER) { - return this.getAdminVolunteerMonthlyAggregatedStats(); + if (user.role === Role.ADMIN) { + return this.getAdminMonthlyAggregatedStats(); + } else if (user.role === Role.VOLUNTEER) { + // Resolved lazily via ModuleRef to avoid a UsersModule <-> VolunteersModule + // circular module dependency. + return this.moduleRef + .get(VolunteersService, { strict: false }) + .getVolunteerDashboardStats(userId); } else if (user.role === Role.PANTRY) { return this.pantriesService.getDashboardStatsForUser(userId); } else if (user.role === Role.FOODMANUFACTURER) { diff --git a/apps/backend/src/volunteers/dtos/volunteer-stats.dto.ts b/apps/backend/src/volunteers/dtos/volunteer-stats.dto.ts new file mode 100644 index 000000000..24628f468 --- /dev/null +++ b/apps/backend/src/volunteers/dtos/volunteer-stats.dto.ts @@ -0,0 +1,5 @@ +export type VolunteerStatsDto = { + 'Food Requests': string; + Orders: string; + Donations: string; +}; diff --git a/apps/backend/src/volunteers/volunteers.module.ts b/apps/backend/src/volunteers/volunteers.module.ts index 01997c832..958fe6121 100644 --- a/apps/backend/src/volunteers/volunteers.module.ts +++ b/apps/backend/src/volunteers/volunteers.module.ts @@ -1,6 +1,7 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from '../users/users.entity'; +import { Order } from '../orders/order.entity'; import { PantriesModule } from '../pantries/pantries.module'; import { AuthModule } from '../auth/auth.module'; import { VolunteersController } from './volunteers.controller'; @@ -12,7 +13,7 @@ import { EmailsModule } from '../emails/email.module'; @Module({ imports: [ - TypeOrmModule.forFeature([User]), + TypeOrmModule.forFeature([User, Order]), UsersModule, forwardRef(() => PantriesModule), forwardRef(() => AuthModule), diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index 8d09e34a1..ccb0f53a5 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -385,4 +385,130 @@ describe('VolunteersService', () => { ); }); }); + + describe('getVolunteerDashboardStats', () => { + const now = new Date(); + const currentMonthDate = new Date( + now.getFullYear(), + now.getMonth(), + 15, + 12, + 0, + 0, + ); + + // Stats are aggregated for the current month, but the seed data uses fixed + // 2024 dates. Pull the relevant records into the current month so the + // month filter includes them. Run this after any assignee updates that + // still rely on the original seeded dates as identifiers. + async function normalizeDatesToCurrentMonth() { + await testDataSource.query(`UPDATE food_requests SET requested_at = $1`, [ + currentMonthDate, + ]); + await testDataSource.query(`UPDATE orders SET created_at = $1`, [ + currentMonthDate, + ]); + await testDataSource.query(`UPDATE donations SET date_donated = $1`, [ + currentMonthDate, + ]); + } + + it('throws NotFoundException for non-existent volunteer', async () => { + await expect(service.getVolunteerDashboardStats(999)).rejects.toThrow( + new NotFoundException('Volunteer 999 not found'), + ); + }); + + it('counts food requests from assigned pantries, with zero orders/donations when none are assigned', async () => { + // Maria Garcia (id=7) is assigned to pantries 2 and 3, each with 1 food request + await testDataSource.query(`UPDATE orders SET assignee_id = NULL`); + await normalizeDatesToCurrentMonth(); + + const stats = await service.getVolunteerDashboardStats(7); + + const expectedKeys = ['Food Requests', 'Orders', 'Donations']; + expect(Object.keys(stats)).toEqual(expectedKeys); + + Object.values(stats).forEach((value) => { + expect(typeof value).toBe('string'); + }); + + expect(stats).toEqual({ + 'Food Requests': '2', + Orders: '0', + Donations: '0', + }); + }); + + it('returns zero stats when the volunteer has no pantry or order assignments', async () => { + await testDataSource.query( + `DELETE FROM "volunteer_assignments" WHERE volunteer_id = 8`, + ); + await testDataSource.query(`UPDATE orders SET assignee_id = NULL`); + await normalizeDatesToCurrentMonth(); + + const stats = await service.getVolunteerDashboardStats(8); + + expect(stats).toEqual({ + 'Food Requests': '0', + Orders: '0', + Donations: '0', + }); + }); + + it('counts orders assigned to the volunteer and the unique donations behind them', async () => { + // James Thomas (id=6) is assigned to pantry 1 only (2 food requests), + // but all 4 seeded orders (spanning 4 distinct donations) are assigned to him + await testDataSource.query(`UPDATE orders SET assignee_id = 6`); + await normalizeDatesToCurrentMonth(); + + const stats = await service.getVolunteerDashboardStats(6); + + expect(stats).toEqual({ + 'Food Requests': '2', + Orders: '4', + Donations: '4', + }); + }); + + it('counts orders/donations assigned directly to the volunteer even outside their assigned pantries', async () => { + // Maria Garcia (id=7) is assigned to pantries 2 and 3, not pantry 1, + // but is directly assigned the delivered order under pantry 1 + await testDataSource.query(`UPDATE orders SET assignee_id = NULL`); + await testDataSource.query( + `UPDATE orders SET assignee_id = 7 WHERE shipped_at = '2024-01-17 08:00:00'`, + ); + await normalizeDatesToCurrentMonth(); + + const stats = await service.getVolunteerDashboardStats(7); + + expect(stats).toEqual({ + 'Food Requests': '2', + Orders: '1', + Donations: '1', + }); + }); + + it('counts a shared donation only once across multiple assigned orders', async () => { + // William Moore (id=8) is assigned to pantry 3 only (1 food request). + // Assign him the Westside order (donation D2 only) and the pending + // Downtown order (donations D4 and D2) - D2 should only count once. + await testDataSource.query(`UPDATE orders SET assignee_id = NULL`); + await testDataSource.query( + `UPDATE orders SET assignee_id = 8 WHERE shipped_at = '2024-01-22 09:00:00'`, + ); + await testDataSource.query( + `UPDATE orders SET assignee_id = 8 WHERE created_at = '2024-02-03 12:00:00' AND status = 'pending'`, + ); + await normalizeDatesToCurrentMonth(); + + const stats = await service.getVolunteerDashboardStats(8); + + expect(stats).toEqual({ + 'Food Requests': '1', + Orders: '2', + Donations: '2', + }); + }); + }); }); diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index d2e1f836a..34e4ceaa3 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -1,4 +1,9 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + forwardRef, + Inject, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from '../users/users.entity'; @@ -10,14 +15,25 @@ import { Assignments, VolunteerOrder } from './types'; import { RequestsService } from '../foodRequests/request.service'; import { OrdersService } from '../orders/order.service'; import { FoodRequestSummaryDto } from '../foodRequests/dtos/food-request-summary.dto'; +import { Order } from '../orders/order.entity'; +import { FoodRequest } from '../foodRequests/request.entity'; +import { Allocation } from '../allocations/allocations.entity'; +import { DonationItem } from '../donationItems/donationItems.entity'; +import { Donation } from '../donations/donations.entity'; +import { VolunteerStatsDto } from './dtos/volunteer-stats.dto'; @Injectable() export class VolunteersService { constructor( @InjectRepository(User) private repo: Repository, + @InjectRepository(Order) + private orderRepo: Repository, + @Inject(forwardRef(() => UsersService)) private usersService: UsersService, + @Inject(forwardRef(() => RequestsService)) private requestsService: RequestsService, + @Inject(forwardRef(() => OrdersService)) private ordersService: OrdersService, ) {} @@ -99,4 +115,67 @@ export class VolunteersService { }, })); } + + async getVolunteerDashboardStats( + volunteerId: number, + ): Promise { + await this.findOne(volunteerId); + + const now = new Date(); + const startMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const endMonth = new Date( + now.getFullYear(), + now.getMonth() + 1, + 0, + 23, + 59, + 59, + 999, + ); + + // Food requests for pantries the volunteer is assigned to, requested this month + const foodRequestsResult = await this.repo + .createQueryBuilder('volunteer') + .leftJoin('volunteer.pantries', 'pantry') + .leftJoin(FoodRequest, 'fr', 'fr.pantry_id = pantry.pantry_id') + .where('volunteer.id = :volunteerId', { volunteerId }) + .andWhere('fr.requested_at BETWEEN :startMonth AND :endMonth', { + startMonth, + endMonth, + }) + .select('COUNT(DISTINCT fr.request_id)', 'food_requests') + .getRawOne(); + + // Orders assigned to the volunteer that were created this month + const ordersResult = await this.orderRepo + .createQueryBuilder('order') + .where('order.assigneeId = :volunteerId', { volunteerId }) + .andWhere('order.created_at BETWEEN :startMonth AND :endMonth', { + startMonth, + endMonth, + }) + .select('COUNT(DISTINCT order.order_id)', 'orders') + .getRawOne(); + + // Unique donations behind the donation items allocated to the volunteer's + // orders, donated this month + const donationsResult = await this.orderRepo + .createQueryBuilder('order') + .leftJoin(Allocation, 'a', 'a.order_id = order.order_id') + .leftJoin(DonationItem, 'di', 'di.item_id = a.item_id') + .leftJoin(Donation, 'd', 'd.donation_id = di.donation_id') + .where('order.assigneeId = :volunteerId', { volunteerId }) + .andWhere('d.date_donated BETWEEN :startMonth AND :endMonth', { + startMonth, + endMonth, + }) + .select('COUNT(DISTINCT di.donation_id)', 'donations') + .getRawOne(); + + return { + 'Food Requests': String(foodRequestsResult.food_requests), + Orders: String(ordersResult.orders), + Donations: String(donationsResult.donations), + }; + } } diff --git a/apps/frontend/src/components/foodRequestManagement.tsx b/apps/frontend/src/components/foodRequestManagement.tsx index c3fa3bc72..6f725a26f 100644 --- a/apps/frontend/src/components/foodRequestManagement.tsx +++ b/apps/frontend/src/components/foodRequestManagement.tsx @@ -396,76 +396,76 @@ const RequestManagement: React.FC = ({ )} ))} + + - {selectedViewDetailsRequest && ( - { - setSelectedViewDetailsRequest(null); - if (initialRequestId) { - navigate(location.pathname, { replace: true }); - } - }} - onSuccess={loadRequests} - onDelete={() => setDeleteRequest(selectedViewDetailsRequest)} - /> - )} + {selectedViewDetailsRequest && ( + { + setSelectedViewDetailsRequest(null); + if (initialRequestId) { + navigate(location.pathname, { replace: true }); + } + }} + onSuccess={loadRequests} + onDelete={() => setDeleteRequest(selectedViewDetailsRequest)} + /> + )} - {deleteRequest && ( - setDeleteRequest(null)} - onSuccess={() => { - setAlertMessage( - 'Successfully deleted food request.', - AlertStatus.INFO, - ); - loadRequests(); - setSelectedViewDetailsRequest(null); - }} - /> - )} + {deleteRequest && ( + setDeleteRequest(null)} + onSuccess={() => { + setAlertMessage( + 'Successfully deleted food request.', + AlertStatus.INFO, + ); + loadRequests(); + setSelectedViewDetailsRequest(null); + }} + /> + )} - {selectedActionRequest && ( - { - setSelectedCloseRequestAction(selectedActionRequest); - }} - onCreateOrder={() => { - setSelectedCreateOrderRequest(selectedActionRequest); - }} - /> - )} + {selectedActionRequest && ( + { + setSelectedCloseRequestAction(selectedActionRequest); + }} + onCreateOrder={() => { + setSelectedCreateOrderRequest(selectedActionRequest); + }} + /> + )} - {selectedCloseRequestAction && ( - { - setAlertMessage('Request Closed', AlertStatus.INFO); - loadRequests(); - }} - /> - )} + {selectedCloseRequestAction && ( + { + setAlertMessage('Request Closed', AlertStatus.INFO); + loadRequests(); + }} + /> + )} - {selectedCreateOrderRequest && ( - { - setAlertMessage('Order Created', AlertStatus.INFO); - loadRequests(); - }} - /> - )} - - + {selectedCreateOrderRequest && ( + { + setAlertMessage('Order Created', AlertStatus.INFO); + loadRequests(); + }} + /> + )} )} diff --git a/apps/frontend/src/components/forms/orderDetailsModal.tsx b/apps/frontend/src/components/forms/orderDetailsModal.tsx index 570602a78..3b85143ab 100644 --- a/apps/frontend/src/components/forms/orderDetailsModal.tsx +++ b/apps/frontend/src/components/forms/orderDetailsModal.tsx @@ -233,297 +233,307 @@ const OrderDetailsModal: React.FC = ({ Fulfilled by {orderDetails?.foodManufacturerName} - {isEditing ? ( - - - {orderDetails?.foodManufacturerName} Stock - - - {Object.entries(groupedManufacturerItems).map( - ([foodType, items]) => ( - - - {foodType} - {foodRequest?.requestedFoodTypes.includes( - foodType as FoodType, - ) && ( - - Matching - - )} - - {items.map((item) => ( - - - {item.itemName} - + + + + Order Details + + + Associated Request + + + + {!foodRequest && ( + + {' '} + No associated food request to display{' '} + + )} - setEditingItemId(item.itemId)} - > - {editingItemId === item.itemId ? ( - - setItemAllocations((prev) => ({ - ...prev, - [item.itemId]: - Number(e.target.value) || 0, - })) - } - onBlur={() => setEditingItemId(null)} - /> - ) : ( - - item.quantity - - item.reservedQuantity + - (currentAllocations[item.itemId] ?? 0) - ? 'red.core' - : 'neutral.800' - } - > - {itemAllocations[item.itemId] ?? 0} of{' '} - {item.quantity - - item.reservedQuantity + - (currentAllocations[item.itemId] ?? 0)} - - )} - - - ))} - - ), - )} - - - - - - - ) : ( - - - - Order Details - - - Associated Request - - - - {!foodRequest && ( - - {' '} - No associated food request to display{' '} - - )} - - {foodRequest && ( - - - - Request {foodRequest.requestId} - - - {' '} - {foodRequest.pantry.pantryName} - + + + Request {foodRequest.requestId} - + + {' '} + {foodRequest.pantry.pantryName} - {foodRequest.status === FoodRequestStatus.CLOSED ? ( - - Closed - - ) : ( - - Active - - )} - + + {foodRequest.status === FoodRequestStatus.CLOSED ? ( + + Closed + + ) : ( + + Active + + )} + - - - - Size of Shipment - - - - - {foodRequest.requestedSize} - - - + + + Size of Shipment + + + + {foodRequest.requestedSize} + + + - - - - Food Type(s) - - + + + + Food Type(s) + + - {foodRequest.requestedFoodTypes.length > 0 && ( - - )} - + {foodRequest.requestedFoodTypes.length > 0 && ( + + )} + - - - - Additional Information - - - - {foodRequest.additionalInformation} + + + + Additional Information - - - )} - + + + {foodRequest.additionalInformation} + + + + )} + - - {Object.entries(groupedOrderItemsByType).map( - ([foodType, items]) => ( - - {foodType} - {items.map((item) => ( - - - {item.name} - + + {isEditing ? ( + + + {orderDetails?.foodManufacturerName} Stock + + + {Object.entries(groupedManufacturerItems).map( + ([foodType, items]) => ( + + + {foodType} + {foodRequest?.requestedFoodTypes.includes( + foodType as FoodType, + ) && ( + + Matching + + )} + + {items.map((item) => ( + + + {item.itemName} + + + setEditingItemId(item.itemId) + } + > + {editingItemId === item.itemId ? ( + + setItemAllocations((prev) => ({ + ...prev, + [item.itemId]: + Number(e.target.value) || 0, + })) + } + onBlur={() => setEditingItemId(null)} + /> + ) : ( + + item.quantity - + item.reservedQuantity + + (currentAllocations[item.itemId] ?? + 0) + ? 'red.core' + : 'neutral.800' + } + > + {itemAllocations[item.itemId] ?? 0} of{' '} + {item.quantity - + item.reservedQuantity + + (currentAllocations[item.itemId] ?? + 0)} + + )} + + + ))} + + ), + )} + + + + + + + ) : ( + <> + {Object.entries(groupedOrderItemsByType).map( + ([foodType, items]) => ( + + {foodType} + {items.map((item) => ( - - {item.quantity} + + {item.name} + + + + {item.quantity} + + - - ))} - - ), - )} - - Tracking - - {orderDetails?.trackingLink ? ( - - {orderDetails.trackingLink} - - ) : ( - - No tracking link available at this time + ))} + + ), + )} + + Tracking - )} - - - )} + {orderDetails?.trackingLink ? ( + + {orderDetails.trackingLink} + + ) : ( + + No tracking link available at this time + + )} + + )} + + diff --git a/apps/frontend/src/components/forms/resetPasswordModal.tsx b/apps/frontend/src/components/forms/resetPasswordModal.tsx index 2aadff9b5..5ed7e14ad 100644 --- a/apps/frontend/src/components/forms/resetPasswordModal.tsx +++ b/apps/frontend/src/components/forms/resetPasswordModal.tsx @@ -9,11 +9,14 @@ import { Button, Link, Field, + IconButton, + Group, } from '@chakra-ui/react'; import { resetPassword, confirmResetPassword } from 'aws-amplify/auth'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../../hooks/alert'; import { useModalBodyCleanup } from '../../hooks/modalBodyCleanup'; +import { Eye, EyeOff } from 'lucide-react'; import { AlertStatus } from '../../types/types'; const ResetPasswordModal: React.FC = () => { @@ -23,6 +26,8 @@ const ResetPasswordModal: React.FC = () => { const [step, setStep] = useState<'reset' | 'new'>('reset'); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [alertState, setAlertMessage] = useAlert(); const navigate = useNavigate(); @@ -165,23 +170,45 @@ const ResetPasswordModal: React.FC = () => { New Password - setPassword(e.target.value)} - onKeyDown={handleResetPasswordKeyDown} - /> + + setPassword(e.target.value)} + onKeyDown={handleResetPasswordKeyDown} + /> + setShowPassword((prev) => !prev)} + > + + {showPassword && } + {!showPassword && } + + + Confirm Password - setConfirmPassword(e.target.value)} - onKeyDown={handleResetPasswordKeyDown} - /> + + setConfirmPassword(e.target.value)} + onKeyDown={handleResetPasswordKeyDown} + /> + setShowConfirmPassword((prev) => !prev)} + > + + {showConfirmPassword && } + {!showConfirmPassword && } + + + )} From b95fa59d8397e81bc9cb5f939ab5354cca94073c Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sat, 25 Jul 2026 15:03:27 -0700 Subject: [PATCH 2/4] Assigned pantries page bugbash fixes --- .../src/pantries/pantries.controller.ts | 10 +++- .../containers/pantryApplicationDetails.tsx | 52 +++++++++++++------ .../containers/volunteerAssignedPantries.tsx | 8 +++ 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index 63503e96a..913ee0998 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -45,10 +45,16 @@ import { FoodRequestSummaryDto } from '../foodRequests/dtos/food-request-summary const resolvePantryAuthorizedUserIds: OwnerIdResolver = ({ entityId, services, + user, }) => pipeNullable( () => services.get(PantriesService).findOne(entityId), - (pantry: Pantry) => [pantry.pantryUser.id], + (pantry: Pantry) => { + if (user?.role === Role.VOLUNTEER) { + return (pantry.volunteers ?? []).map((volunteer) => volunteer.id); + } + return [pantry.pantryUser.id]; + }, ); @Controller('pantries') @@ -119,7 +125,7 @@ export class PantriesController { idParam: 'pantryId', resolver: resolvePantryAuthorizedUserIds, }) - @Roles(Role.PANTRY, Role.ADMIN) + @Roles(Role.PANTRY, Role.ADMIN, Role.VOLUNTEER) @Get('/:pantryId') async getPantry( @Param('pantryId', ParseIntPipe) pantryId: number, diff --git a/apps/frontend/src/containers/pantryApplicationDetails.tsx b/apps/frontend/src/containers/pantryApplicationDetails.tsx index 33cdee951..a053c78dc 100644 --- a/apps/frontend/src/containers/pantryApplicationDetails.tsx +++ b/apps/frontend/src/containers/pantryApplicationDetails.tsx @@ -12,7 +12,12 @@ import { Spinner, } from '@chakra-ui/react'; import ApiClient from '@api/apiClient'; -import { AlertStatus, ApplicationStatus, PantryWithUser } from '../types/types'; +import { + AlertStatus, + ApplicationStatus, + PantryWithUser, + Role, +} from '../types/types'; import { formatDate, formatPhone } from '@utils/utils'; import { TagGroup } from '@components/forms/tagGroup'; import { Pencil, TriangleAlert } from 'lucide-react'; @@ -101,6 +106,7 @@ const PantryApplicationDetails: React.FC = () => { const [showApproveModal, setShowApproveModal] = useState(false); const [showDenyModal, setShowDenyModal] = useState(false); const [isEditing, setIsEditing] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); const fieldContentStyles = { textStyle: 'p2', @@ -156,6 +162,18 @@ const PantryApplicationDetails: React.FC = () => { fetchApplicationDetails(); }, [fetchApplicationDetails]); + useEffect(() => { + const fetchRole = async () => { + try { + const user = await ApiClient.getMe(); + setIsAdmin(user.role === Role.ADMIN); + } catch { + setIsAdmin(false); + } + }; + fetchRole(); + }, []); + const handleApprove = async () => { if (application) { try { @@ -252,25 +270,27 @@ const PantryApplicationDetails: React.FC = () => { {application.pantryName} - setIsEditing(true)} - > - - - {isEditing ? 'Editing' : 'Edit'} - - + {isAdmin && ( + setIsEditing(true)} + > + + + {isEditing ? 'Editing' : 'Edit'} + + + )} )} - {!isApplicationMode && isEditing ? ( + {!isApplicationMode && isAdmin && isEditing ? ( { textDecoration="underline" cursor="pointer" color="gray.dark" + onClick={() => + navigate( + ROUTES.PANTRY_MANAGEMENT_DETAILS.replace( + ':pantryId', + pantry.pantryId.toString(), + ), + ) + } > {pantry.pantryName} From fa6818daa3beb9760edf5f3f9663e36c5ba02da1 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Thu, 30 Jul 2026 01:41:28 -0700 Subject: [PATCH 3/4] Dashboard fixes --- apps/backend/src/users/users.service.spec.ts | 176 +++++------------- apps/backend/src/users/users.service.ts | 30 +-- .../src/volunteers/volunteers.service.spec.ts | 31 --- .../src/volunteers/volunteers.service.ts | 30 +-- .../src/containers/pantryDashboard.tsx | 16 +- .../src/containers/volunteerDashboard.tsx | 15 +- 6 files changed, 73 insertions(+), 225 deletions(-) diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 311820bfb..9c6826c27 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -513,41 +513,9 @@ describe('UsersService', () => { }); }); - describe('getAdminMonthlyAggregatedStats', () => { - it('should return correct aggregated counts for the current month', async () => { - const foodRequestRepo = testDataSource.getRepository(FoodRequest); - - const now = new Date(); - - const createDonationBody: Partial = { - foodManufacturerId: 1, - recurrence: RecurrenceEnum.MONTHLY, - recurrenceFreq: 3, - occurrencesRemaining: 2, - items: [ - { - itemName: 'Test Item', - quantity: 10, - foodType: FoodType.GRANOLA, - ozPerItem: 3.4, - estimatedValue: 3.4, - foodRescue: false, - }, - ], - }; - - await donationService.create(createDonationBody as CreateDonationDto); - - // updating existing request to have a current month requested at date - const existingRequest = await foodRequestService.findOne(1); - existingRequest.requestedAt = new Date( - now.getFullYear(), - now.getMonth(), - 5, - ); - await foodRequestRepo.save(existingRequest); - - const stats = await service.getAdminMonthlyAggregatedStats(); + describe('getAdminAggregatedStats', () => { + it('should return all-time aggregated counts across all records', async () => { + const stats = await service.getAdminAggregatedStats(); const expectedKeys = [ 'Food Requests', @@ -562,120 +530,62 @@ describe('UsersService', () => { expect(typeof value).toBe('string'); }); + // Seed data contains 4 food requests, 4 orders, 4 donations, and 4 + // volunteers, all dated in 2024. All-time stats count them regardless + // of when they were created. expect(stats).toEqual({ - 'Food Requests': '1', - Orders: '0', - Donations: '1', + 'Food Requests': '4', + Orders: '4', + Donations: '4', Volunteers: '4', }); }); - it('should return correct aggregated counts for the current month with edge cases of start and end of month', async () => { + it('should count records regardless of how old their dates are', async () => { const foodRequestRepo = testDataSource.getRepository(FoodRequest); - const now = new Date(); - - const year = now.getFullYear(); - const month = now.getMonth(); - - const startOfMonth = new Date(year, month, 1, 0, 0, 0, 0); - - const endOfMonth = new Date(year, month + 1, 0, 23, 59, 59, 999); - - const existingRequest1 = await foodRequestService.findOne(1); - existingRequest1.requestedAt = endOfMonth; - await foodRequestRepo.save(existingRequest1); - - const existingRequest2 = await foodRequestService.findOne(2); - existingRequest2.requestedAt = startOfMonth; - await foodRequestRepo.save(existingRequest2); - - const stats = await service.getAdminMonthlyAggregatedStats(); - - const expectedKeys = [ - 'Food Requests', - 'Orders', - 'Donations', - 'Volunteers', - ]; - - expect(Object.keys(stats)).toEqual(expectedKeys); - - Object.values(stats).forEach((value) => { - expect(typeof value).toBe('string'); - }); - - expect(stats).toEqual({ - 'Food Requests': '2', - Orders: '0', - Donations: '0', - Volunteers: '4', - }); - }); - - it('should return just volunteer count if no other fields are relative to current month', async () => { - const stats = await service.getAdminMonthlyAggregatedStats(); - - const expectedKeys = [ - 'Food Requests', - 'Orders', - 'Donations', - 'Volunteers', - ]; + // Move a request far into the past to confirm dates no longer filter + // the results. + const existingRequest = await foodRequestService.findOne(1); + existingRequest.requestedAt = new Date('2000-01-01T00:00:00Z'); + await foodRequestRepo.save(existingRequest); - expect(Object.keys(stats)).toEqual(expectedKeys); - - Object.values(stats).forEach((value) => { - expect(typeof value).toBe('string'); - }); + const stats = await service.getAdminAggregatedStats(); expect(stats).toEqual({ - 'Food Requests': '0', - Orders: '0', - Donations: '0', + 'Food Requests': '4', + Orders: '4', + Donations: '4', Volunteers: '4', }); }); - it('should return correct aggregated counts for mixed month dataset', async () => { - const foodRequestRepo = testDataSource.getRepository(FoodRequest); - - const now = new Date(); - - const year = now.getFullYear(); - const month = now.getMonth(); - - const startOfCurrentMonth = new Date(year, month, 1, 0, 0, 0, 0); - - const endOfNextMonth = new Date(year, month + 2, 0, 23, 59, 59, 999); - - const existingRequest1 = await foodRequestService.findOne(1); - existingRequest1.requestedAt = endOfNextMonth; - await foodRequestRepo.save(existingRequest1); - - const existingRequest2 = await foodRequestService.findOne(2); - existingRequest2.requestedAt = startOfCurrentMonth; - await foodRequestRepo.save(existingRequest2); - - const stats = await service.getAdminMonthlyAggregatedStats(); - - const expectedKeys = [ - 'Food Requests', - 'Orders', - 'Donations', - 'Volunteers', - ]; + it('should reflect newly created records in the counts', async () => { + const createDonationBody: Partial = { + foodManufacturerId: 1, + recurrence: RecurrenceEnum.MONTHLY, + recurrenceFreq: 3, + occurrencesRemaining: 2, + items: [ + { + itemName: 'Test Item', + quantity: 10, + foodType: FoodType.GRANOLA, + ozPerItem: 3.4, + estimatedValue: 3.4, + foodRescue: false, + }, + ], + }; - expect(Object.keys(stats)).toEqual(expectedKeys); + await donationService.create(createDonationBody as CreateDonationDto); - Object.values(stats).forEach((value) => { - expect(typeof value).toBe('string'); - }); + const stats = await service.getAdminAggregatedStats(); expect(stats).toEqual({ - 'Food Requests': '1', - Orders: '0', - Donations: '0', + 'Food Requests': '4', + Orders: '4', + Donations: '5', Volunteers: '4', }); }); @@ -695,8 +605,8 @@ describe('UsersService', () => { }); describe('getUserDashboardStats', () => { - it('should call getAdminMonthlyAggregatedStats for admin user', async () => { - const spy = jest.spyOn(service, 'getAdminMonthlyAggregatedStats'); + it('should call getAdminAggregatedStats for admin user', async () => { + const spy = jest.spyOn(service, 'getAdminAggregatedStats'); const result = await service.getUserDashboardStats(1); diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 4c4c42159..6368ee106 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -8,7 +8,7 @@ import { } from '@nestjs/common'; import { ModuleRef } from '@nestjs/core'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; -import { Between, DataSource, In, Repository } from 'typeorm'; +import { DataSource, In, Repository } from 'typeorm'; import { User } from './users.entity'; import { PendingApplication, Role } from './types'; import { validateId } from '../utils/validation.utils'; @@ -272,30 +272,12 @@ export class UsersService { return user; } - async getAdminMonthlyAggregatedStats(): Promise { - const now = new Date(); - const startMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const endMonth = new Date( - now.getFullYear(), - now.getMonth() + 1, - 0, - 23, - 59, - 59, - 999, - ); - + async getAdminAggregatedStats(): Promise { const [foodRequestsCount, ordersCount, donationsCount, volunteersCount] = await Promise.all([ - this.foodRequestRepo.count({ - where: { requestedAt: Between(startMonth, endMonth) }, - }), - this.orderRepo.count({ - where: { createdAt: Between(startMonth, endMonth) }, - }), - this.donationRepo.count({ - where: { dateDonated: Between(startMonth, endMonth) }, - }), + this.foodRequestRepo.count(), + this.orderRepo.count(), + this.donationRepo.count(), this.repo.count({ where: { role: Role.VOLUNTEER } }), ]); @@ -315,7 +297,7 @@ export class UsersService { const user = await this.findOne(userId); if (user.role === Role.ADMIN) { - return this.getAdminMonthlyAggregatedStats(); + return this.getAdminAggregatedStats(); } else if (user.role === Role.VOLUNTEER) { // Resolved lazily via ModuleRef to avoid a UsersModule <-> VolunteersModule // circular module dependency. diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index ccb0f53a5..7ac07f5b0 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -387,32 +387,6 @@ describe('VolunteersService', () => { }); describe('getVolunteerDashboardStats', () => { - const now = new Date(); - const currentMonthDate = new Date( - now.getFullYear(), - now.getMonth(), - 15, - 12, - 0, - 0, - ); - - // Stats are aggregated for the current month, but the seed data uses fixed - // 2024 dates. Pull the relevant records into the current month so the - // month filter includes them. Run this after any assignee updates that - // still rely on the original seeded dates as identifiers. - async function normalizeDatesToCurrentMonth() { - await testDataSource.query(`UPDATE food_requests SET requested_at = $1`, [ - currentMonthDate, - ]); - await testDataSource.query(`UPDATE orders SET created_at = $1`, [ - currentMonthDate, - ]); - await testDataSource.query(`UPDATE donations SET date_donated = $1`, [ - currentMonthDate, - ]); - } - it('throws NotFoundException for non-existent volunteer', async () => { await expect(service.getVolunteerDashboardStats(999)).rejects.toThrow( new NotFoundException('Volunteer 999 not found'), @@ -422,7 +396,6 @@ describe('VolunteersService', () => { it('counts food requests from assigned pantries, with zero orders/donations when none are assigned', async () => { // Maria Garcia (id=7) is assigned to pantries 2 and 3, each with 1 food request await testDataSource.query(`UPDATE orders SET assignee_id = NULL`); - await normalizeDatesToCurrentMonth(); const stats = await service.getVolunteerDashboardStats(7); @@ -445,7 +418,6 @@ describe('VolunteersService', () => { `DELETE FROM "volunteer_assignments" WHERE volunteer_id = 8`, ); await testDataSource.query(`UPDATE orders SET assignee_id = NULL`); - await normalizeDatesToCurrentMonth(); const stats = await service.getVolunteerDashboardStats(8); @@ -460,7 +432,6 @@ describe('VolunteersService', () => { // James Thomas (id=6) is assigned to pantry 1 only (2 food requests), // but all 4 seeded orders (spanning 4 distinct donations) are assigned to him await testDataSource.query(`UPDATE orders SET assignee_id = 6`); - await normalizeDatesToCurrentMonth(); const stats = await service.getVolunteerDashboardStats(6); @@ -478,7 +449,6 @@ describe('VolunteersService', () => { await testDataSource.query( `UPDATE orders SET assignee_id = 7 WHERE shipped_at = '2024-01-17 08:00:00'`, ); - await normalizeDatesToCurrentMonth(); const stats = await service.getVolunteerDashboardStats(7); @@ -500,7 +470,6 @@ describe('VolunteersService', () => { await testDataSource.query( `UPDATE orders SET assignee_id = 8 WHERE created_at = '2024-02-03 12:00:00' AND status = 'pending'`, ); - await normalizeDatesToCurrentMonth(); const stats = await service.getVolunteerDashboardStats(8); diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index 34e4ceaa3..d27d50369 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -121,54 +121,30 @@ export class VolunteersService { ): Promise { await this.findOne(volunteerId); - const now = new Date(); - const startMonth = new Date(now.getFullYear(), now.getMonth(), 1); - const endMonth = new Date( - now.getFullYear(), - now.getMonth() + 1, - 0, - 23, - 59, - 59, - 999, - ); - - // Food requests for pantries the volunteer is assigned to, requested this month + // Food requests for pantries the volunteer is assigned to const foodRequestsResult = await this.repo .createQueryBuilder('volunteer') .leftJoin('volunteer.pantries', 'pantry') .leftJoin(FoodRequest, 'fr', 'fr.pantry_id = pantry.pantry_id') .where('volunteer.id = :volunteerId', { volunteerId }) - .andWhere('fr.requested_at BETWEEN :startMonth AND :endMonth', { - startMonth, - endMonth, - }) .select('COUNT(DISTINCT fr.request_id)', 'food_requests') .getRawOne(); - // Orders assigned to the volunteer that were created this month + // Orders assigned to the volunteer const ordersResult = await this.orderRepo .createQueryBuilder('order') .where('order.assigneeId = :volunteerId', { volunteerId }) - .andWhere('order.created_at BETWEEN :startMonth AND :endMonth', { - startMonth, - endMonth, - }) .select('COUNT(DISTINCT order.order_id)', 'orders') .getRawOne(); // Unique donations behind the donation items allocated to the volunteer's - // orders, donated this month + // orders const donationsResult = await this.orderRepo .createQueryBuilder('order') .leftJoin(Allocation, 'a', 'a.order_id = order.order_id') .leftJoin(DonationItem, 'di', 'di.item_id = a.item_id') .leftJoin(Donation, 'd', 'd.donation_id = di.donation_id') .where('order.assigneeId = :volunteerId', { volunteerId }) - .andWhere('d.date_donated BETWEEN :startMonth AND :endMonth', { - startMonth, - endMonth, - }) .select('COUNT(DISTINCT di.donation_id)', 'donations') .getRawOne(); diff --git a/apps/frontend/src/containers/pantryDashboard.tsx b/apps/frontend/src/containers/pantryDashboard.tsx index 823813f22..68aed6a2f 100644 --- a/apps/frontend/src/containers/pantryDashboard.tsx +++ b/apps/frontend/src/containers/pantryDashboard.tsx @@ -14,6 +14,7 @@ import { useAlert } from '../hooks/alert'; import { ROUTES } from '../routes'; import { AlertStatus, + FoodRequestStatus, FoodRequestSummaryDto, OrderSummary, PantryWithUser, @@ -48,11 +49,16 @@ const PantryDashboard: React.FC = () => { const fetchFoodRequests = async () => { try { const pantryFoodRequests = await ApiClient.getPantryRequests(); - const sortedFoodRequests = pantryFoodRequests.sort( - (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => - new Date(b.requestedAt).getTime() - - new Date(a.requestedAt).getTime(), - ); + const sortedFoodRequests = pantryFoodRequests + .filter( + (fr: FoodRequestSummaryDto) => + fr.status === FoodRequestStatus.ACTIVE, + ) + .sort( + (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => + new Date(b.requestedAt).getTime() - + new Date(a.requestedAt).getTime(), + ); setRecentFoodRequests(sortedFoodRequests.slice(0, 2)); } catch { setAlertMessage('Error fetching food requests', AlertStatus.ERROR); diff --git a/apps/frontend/src/containers/volunteerDashboard.tsx b/apps/frontend/src/containers/volunteerDashboard.tsx index 4f93283c3..8eba47e2a 100644 --- a/apps/frontend/src/containers/volunteerDashboard.tsx +++ b/apps/frontend/src/containers/volunteerDashboard.tsx @@ -14,6 +14,7 @@ import { useAlert } from '../hooks/alert'; import { ROUTES } from '../routes'; import { AlertStatus, + FoodRequestStatus, FoodRequestSummaryDto, User, VolunteerOrder, @@ -52,11 +53,15 @@ const VolunteerDashboard: React.FC = () => { ApiClient.getVolunteerRecentOrders(), ]); - const sorted = requests.sort( - (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => - new Date(b.requestedAt).getTime() - - new Date(a.requestedAt).getTime(), - ); + const sorted = requests + .filter( + (r: FoodRequestSummaryDto) => r.status === FoodRequestStatus.ACTIVE, + ) + .sort( + (a: FoodRequestSummaryDto, b: FoodRequestSummaryDto) => + new Date(b.requestedAt).getTime() - + new Date(a.requestedAt).getTime(), + ); setRecentFoodRequests(sorted.slice(0, 2)); setRecentOrders(orders); } catch { From 92e78cbb0523fd399f22683cd5336f734fc4de3a Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Thu, 30 Jul 2026 02:11:59 -0700 Subject: [PATCH 4/4] Dashboard fixes --- apps/backend/src/donations/donations.service.ts | 3 +++ apps/backend/src/foodRequests/request.service.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/backend/src/donations/donations.service.ts b/apps/backend/src/donations/donations.service.ts index 0e54a70ec..8b879df54 100644 --- a/apps/backend/src/donations/donations.service.ts +++ b/apps/backend/src/donations/donations.service.ts @@ -1,5 +1,7 @@ import { BadRequestException, + forwardRef, + Inject, Injectable, Logger, NotFoundException, @@ -33,6 +35,7 @@ export class DonationService { @InjectRepository(DonationItem) private donationItemsRepo: Repository, private donationItemsService: DonationItemsService, + @Inject(forwardRef(() => FoodManufacturersService)) private foodManufacturersService: FoodManufacturersService, @InjectDataSource() private dataSource: DataSource, private emailsService: EmailsService, diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index a7244e8a7..421059259 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -1,5 +1,7 @@ import { BadRequestException, + forwardRef, + Inject, Injectable, InternalServerErrorException, NotFoundException, @@ -40,6 +42,7 @@ export class RequestsService { @InjectRepository(DonationItem) private donationItemRepo: Repository, private emailsService: EmailsService, + @Inject(forwardRef(() => UsersService)) private usersService: UsersService, ) {}