From 8e7df4e18cc3d23b6e7753051ced00e3efda6936 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sat, 21 Mar 2026 17:55:30 -0400 Subject: [PATCH 1/6] Fully functional page and backend adjustments --- .../src/pantries/pantries.controller.spec.ts | 39 ++ .../src/pantries/pantries.controller.ts | 6 + .../src/pantries/pantries.service.spec.ts | 84 +++- apps/backend/src/pantries/pantries.service.ts | 56 ++- apps/backend/src/pantries/types.ts | 5 +- apps/frontend/src/api/apiClient.ts | 30 ++ apps/frontend/src/app.tsx | 9 + .../src/containers/adminDonationStats.tsx | 417 ++++++++++++++++++ apps/frontend/src/containers/homepage.tsx | 7 + apps/frontend/src/theme.ts | 1 + apps/frontend/src/types/types.ts | 4 +- 11 files changed, 627 insertions(+), 31 deletions(-) create mode 100644 apps/frontend/src/containers/adminDonationStats.tsx diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index 0d61827f..2ef6be61 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -108,6 +108,29 @@ describe('PantriesController', () => { expect(controller).toBeDefined(); }); + describe('getApprovedPantryNames', () => { + it('should return an array of approved pantry names', async () => { + const mockNames = ['Pantry A', 'Pantry B']; + mockPantriesService.getApprovedPantryNames.mockResolvedValueOnce( + mockNames, + ); + + const result = await controller.getApprovedPantryNames(); + + expect(result).toEqual(mockNames); + expect(mockPantriesService.getApprovedPantryNames).toHaveBeenCalled(); + }); + + it('should return an empty array if no approved pantries exist', async () => { + mockPantriesService.getApprovedPantryNames.mockResolvedValueOnce([]); + + const result = await controller.getApprovedPantryNames(); + + expect(result).toEqual([]); + expect(mockPantriesService.getApprovedPantryNames).toHaveBeenCalled(); + }); + }); + describe('getPendingPantries', () => { it('should return an array of pending pantries', async () => { mockPantriesService.getPendingPantries.mockResolvedValueOnce([ @@ -275,6 +298,7 @@ describe('PantriesController', () => { const mockStats: PantryStats[] = [ { pantryId: 1, + pantryName: 'Community Food Pantry Downtown', totalItems: 100, totalOz: 1600, totalLbs: 100, @@ -310,6 +334,21 @@ describe('PantriesController', () => { page, ); }); + + it('should propagate NotFoundException when a non-approved pantry name is queried', async () => { + mockPantriesService.getPantryStats.mockRejectedValueOnce( + new Error('Pantries not found: Riverside Food Assistance'), + ); + + await expect( + controller.getPantryStats(['Riverside Food Assistance']), + ).rejects.toThrow('Pantries not found: Riverside Food Assistance'); + expect(mockPantriesService.getPantryStats).toHaveBeenCalledWith( + ['Riverside Food Assistance'], + undefined, + 1, + ); + }); }); describe('getTotalStats', () => { diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index f99e0d35..83628aa3 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -75,6 +75,12 @@ export class PantriesController { return this.pantriesService.getPendingPantries(); } + @Roles(Role.ADMIN) + @Get('/approved-names') + async getApprovedPantryNames(): Promise { + return this.pantriesService.getApprovedPantryNames(); + } + @CheckOwnership({ idParam: 'pantryId', resolver: async ({ entityId, services }) => { diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts index e6ed91f0..a65ed1df 100644 --- a/apps/backend/src/pantries/pantries.service.spec.ts +++ b/apps/backend/src/pantries/pantries.service.spec.ts @@ -325,19 +325,16 @@ describe('PantriesService', () => { expect(stats.percentageFoodRescueItems).toBe(0); }); - it('returns zeroed stats for a pantry with no orders (Riverside Food Assistance)', async () => { - const stats = ( - await service.getPantryStats(['Riverside Food Assistance']) - )[0]; + it('throws NotFoundException for a non-approved (denied) pantry', async () => { + await expect( + service.getPantryStats(['Riverside Food Assistance']), + ).rejects.toThrow(NotFoundException); + }); - expect(stats.pantryId).toBe(4); - expect(stats.totalItems).toBe(0); - expect(stats.totalOz).toBe(0); - expect(stats.totalLbs).toBe(0); - expect(stats.totalDonatedFoodValue).toBe(0); - expect(stats.totalShippingCost).toBe(0); - expect(stats.totalValue).toBe(0); - expect(stats.percentageFoodRescueItems).toBe(0); + it('throws NotFoundException for a non-approved (pending) pantry', async () => { + await expect( + service.getPantryStats(['Harbor Community Center']), + ).rejects.toThrow(NotFoundException); }); it('respects year filter and returns zeros for a non-matching year', async () => { @@ -398,6 +395,9 @@ describe('PantriesService', () => { for (let i = 0; i < 10; i++) { await service.addPantry(makePantryDto(i)); } + await testDataSource.query( + `UPDATE public.pantries SET status = 'approved' WHERE pantry_name LIKE 'BulkTest Pantry%'`, + ); const page1 = await service.getPantryStats(undefined, undefined, 1); expect(page1.length).toBe(10); @@ -425,9 +425,13 @@ describe('PantriesService', () => { expect(community?.totalDonatedFoodValue).toBeCloseTo(130.0, 2); }); - it('returns proper array for no pantryNames given', async () => { + it('returns only approved pantries when no names given', async () => { const stats = await service.getPantryStats(); - expect(stats.length).toBe(6); + expect(stats.length).toBe(3); + const names = stats.map((s) => s.pantryName); + expect(names).not.toContain('Riverside Food Assistance'); + expect(names).not.toContain('Harbor Community Center'); + expect(names).not.toContain('Southside Pantry Network'); }); it('returns nothing for an invalid pantry name', async () => { @@ -447,10 +451,13 @@ describe('PantriesService', () => { }); it('validates all names before paginating — throws if any name is invalid regardless of page', async () => { - // Create 12 valid pantries so we have enough to paginate + // Create 12 valid approved pantries so we have enough to paginate for (let i = 0; i < 12; i++) { await service.addPantry(makePantryDto(i)); } + await testDataSource.query( + `UPDATE public.pantries SET status = 'approved' WHERE pantry_name LIKE 'BulkTest Pantry%'`, + ); const validNames = Array.from( { length: 12 }, (_, i) => `BulkTest Pantry ${i}`, @@ -469,6 +476,9 @@ describe('PantriesService', () => { for (let i = 0; i < 12; i++) { await service.addPantry(makePantryDto(i)); } + await testDataSource.query( + `UPDATE public.pantries SET status = 'approved' WHERE pantry_name LIKE 'BulkTest Pantry%'`, + ); const names = Array.from( { length: 12 }, (_, i) => `BulkTest Pantry ${i}`, @@ -482,6 +492,9 @@ describe('PantriesService', () => { for (let i = 0; i < 12; i++) { await service.addPantry(makePantryDto(i)); } + await testDataSource.query( + `UPDATE public.pantries SET status = 'approved' WHERE pantry_name LIKE 'BulkTest Pantry%'`, + ); const names = Array.from( { length: 12 }, (_, i) => `BulkTest Pantry ${i}`, @@ -495,6 +508,9 @@ describe('PantriesService', () => { for (let i = 0; i < 12; i++) { await service.addPantry(makePantryDto(i)); } + await testDataSource.query( + `UPDATE public.pantries SET status = 'approved' WHERE pantry_name LIKE 'BulkTest Pantry%'`, + ); const names = Array.from( { length: 12 }, (_, i) => `BulkTest Pantry ${i}`, @@ -508,6 +524,9 @@ describe('PantriesService', () => { for (let i = 0; i < 12; i++) { await service.addPantry(makePantryDto(i)); } + await testDataSource.query( + `UPDATE public.pantries SET status = 'approved' WHERE pantry_name LIKE 'BulkTest Pantry%'`, + ); const names = Array.from( { length: 12 }, (_, i) => `BulkTest Pantry ${i}`, @@ -524,6 +543,27 @@ describe('PantriesService', () => { }); }); + describe('getApprovedPantryNames', () => { + it('returns the 3 approved pantry names from seed data', async () => { + const names = await service.getApprovedPantryNames(); + + expect(names).toHaveLength(3); + expect(names).toContain('Community Food Pantry Downtown'); + expect(names).toContain('Westside Community Kitchen'); + expect(names).toContain('North End Food Bank'); + }); + + it('returns an empty array when no pantries are approved', async () => { + await testDataSource.query( + `UPDATE public.pantries SET status = 'pending' WHERE status = 'approved'`, + ); + + const names = await service.getApprovedPantryNames(); + + expect(names).toEqual([]); + }); + }); + describe('getTotalStats', () => { it('aggregates stats across all pantries and matches migration sums', async () => { const total = await service.getTotalStats(); @@ -546,6 +586,20 @@ describe('PantriesService', () => { expect(totalEmpty.totalValue).toBe(0); expect(totalEmpty.percentageFoodRescueItems).toBe(0); }); + + it('returns all zeros when no approved pantries exist', async () => { + await testDataSource.query( + `UPDATE public.pantries SET status = 'pending' WHERE status = 'approved'`, + ); + const total = await service.getTotalStats(); + expect(total.totalItems).toBe(0); + expect(total.totalOz).toBe(0); + expect(total.totalLbs).toBe(0); + expect(total.totalDonatedFoodValue).toBe(0); + expect(total.totalShippingCost).toBe(0); + expect(total.totalValue).toBe(0); + expect(total.percentageFoodRescueItems).toBe(0); + }); }); describe('findByIds', () => { diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts index 542cf100..5796b332 100644 --- a/apps/backend/src/pantries/pantries.service.ts +++ b/apps/backend/src/pantries/pantries.service.ts @@ -43,7 +43,7 @@ export class PantriesService { return pantry; } - private readonly EMPTY_STATS: Omit = { + private readonly EMPTY_STATS: Omit = { totalItems: 0, totalOz: 0, totalLbs: 0, @@ -56,7 +56,7 @@ export class PantriesService { private async aggregateStats( pantryIds?: number[], years?: number[], - ): Promise { + ): Promise[]> { // Query 1: aggregate item stats (totalItems, totalOz, totalDonatedFoodValue, totalFoodRescueItems) const itemsQb = this.orderRepo .createQueryBuilder('order') @@ -137,7 +137,7 @@ export class PantriesService { totalItems > 0 ? parseFloat(((totalFoodRescueItems / totalItems) * 100).toFixed(2)) : 0, - } satisfies PantryStats; + } satisfies Omit; }); } @@ -162,7 +162,10 @@ export class PantriesService { if (nameArray?.length) { const allMatched = await this.repo.find({ select: ['pantryId', 'pantryName'], - where: { pantryName: In(nameArray) }, + where: { + pantryName: In(nameArray), + status: ApplicationStatus.APPROVED, + }, order: { pantryId: 'ASC' }, }); @@ -189,14 +192,20 @@ export class PantriesService { const stats = await this.aggregateStats(pantryIds, yearsArray); const statsMap = new Map(stats.map((s) => [s.pantryId, s])); - return pantryIds.map( - (id) => statsMap.get(id) ?? { pantryId: id, ...this.EMPTY_STATS }, - ); + const nameMap = new Map(paginated.map((p) => [p.pantryId, p.pantryName])); + return pantryIds.map((id) => { + const stat = statsMap.get(id); + const pantryName = nameMap.get(id) ?? ''; + return stat + ? { ...stat, pantryName } + : { pantryId: id, pantryName, ...this.EMPTY_STATS }; + }); } - // No names provided — paginate from the full table + // No names provided — paginate from approved pantries only const pantries = await this.repo.find({ select: ['pantryId', 'pantryName'], + where: { status: ApplicationStatus.APPROVED }, order: { pantryId: 'ASC' }, skip: (page - 1) * PAGE_SIZE, take: PAGE_SIZE, @@ -211,17 +220,32 @@ export class PantriesService { const stats = await this.aggregateStats(pantryIds, yearsArray); const statsMap = new Map(stats.map((s) => [s.pantryId, s])); - return pantryIds.map( - (id) => statsMap.get(id) ?? { pantryId: id, ...this.EMPTY_STATS }, - ); + const nameMap = new Map(pantries.map((p) => [p.pantryId, p.pantryName])); + return pantryIds.map((id) => { + const stat = statsMap.get(id); + const pantryName = nameMap.get(id) ?? ''; + return stat + ? { ...stat, pantryName } + : { pantryId: id, pantryName, ...this.EMPTY_STATS }; + }); } async getTotalStats(years?: number[]): Promise { + const approvedPantries = await this.repo.find({ + select: ['pantryId'], + where: { status: ApplicationStatus.APPROVED }, + }); + + if (approvedPantries.length === 0) { + return { ...this.EMPTY_STATS }; + } + + const approvedPantryIds = approvedPantries.map((p) => p.pantryId); const yearsArray = years ? (Array.isArray(years) ? years : [years]).map(Number) : undefined; - const stats = await this.aggregateStats(undefined, yearsArray); + const stats = await this.aggregateStats(approvedPantryIds, yearsArray); const totalStats = { ...this.EMPTY_STATS }; let totalFoodRescueItems = 0; @@ -254,6 +278,14 @@ export class PantriesService { }); } + async getApprovedPantryNames(): Promise { + const pantries = await this.repo.find({ + select: ['pantryName'], + where: { status: ApplicationStatus.APPROVED }, + }); + return pantries.map((p) => p.pantryName); + } + async addPantry(pantryData: PantryApplicationDto) { const pantryContact: User = new User(); const pantry: Pantry = new Pantry(); diff --git a/apps/backend/src/pantries/types.ts b/apps/backend/src/pantries/types.ts index 0fb3f495..bed18169 100644 --- a/apps/backend/src/pantries/types.ts +++ b/apps/backend/src/pantries/types.ts @@ -42,6 +42,7 @@ export enum ReserveFoodForAllergic { export type PantryStats = { pantryId: number; + pantryName: string; totalItems: number; totalOz: number; totalLbs: number; @@ -51,5 +52,5 @@ export type PantryStats = { percentageFoodRescueItems: number; }; -// Make new type that is just a list of PantryStats with pantryId omitted -export type TotalStats = Omit; +// Make new type that is just a list of PantryStats with pantryId and pantryName omitted +export type TotalStats = Omit; diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 2a4a1a41..14febb79 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -25,6 +25,8 @@ import { FoodRequestSummaryDto, PantryWithUser, Assignments, + PantryStats, + TotalStats, } from 'types/types'; const defaultBaseUrl = @@ -179,6 +181,34 @@ export class ApiClient { return this.axiosInstance.post(`/api/pantries`, data); } + public async getPantryStats(params?: { + pantryNames?: string[]; + page?: number; + }): Promise { + return this.axiosInstance + .get('/api/pantries/stats-by-pantry', { + params: { + ...(params?.pantryNames?.length && { + pantryNames: params.pantryNames, + }), + ...(params?.page && { page: params.page }), + }, + }) + .then((response) => response.data); + } + + public async getTotalStats(): Promise { + return this.axiosInstance + .get('/api/pantries/total-stats') + .then((response) => response.data); + } + + public async getApprovedPantryNames(): Promise { + return this.axiosInstance + .get('/api/pantries/approved-names') + .then((response) => response.data); + } + public async getFoodRequestFromOrder( orderId: number, ): Promise { diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx index f41edb4b..70cab670 100644 --- a/apps/frontend/src/app.tsx +++ b/apps/frontend/src/app.tsx @@ -30,6 +30,7 @@ import FoodManufacturerApplication from '@containers/foodManufacturerApplication import { submitManufacturerApplicationForm } from '@components/forms/manufacturerApplicationForm'; import AssignedPantries from '@containers/volunteerAssignedPantries'; import VolunteerRequestManagement from '@containers/volunteerRequestManagement'; +import AdminDonationStats from '@containers/adminDonationStats'; Amplify.configure(CognitoAuthConfig); @@ -171,6 +172,14 @@ const router = createBrowserRouter([ ), }, + { + path: '/admin-donation-stats', + element: ( + + + + ), + }, { path: '/volunteer-management', element: ( diff --git a/apps/frontend/src/containers/adminDonationStats.tsx b/apps/frontend/src/containers/adminDonationStats.tsx new file mode 100644 index 00000000..eda6b22e --- /dev/null +++ b/apps/frontend/src/containers/adminDonationStats.tsx @@ -0,0 +1,417 @@ +import React, { useState, useEffect } from 'react'; +import { + ArrowDownUp, + ChevronRight, + ChevronLeft, + Funnel, + Search, +} from 'lucide-react'; +import { + Box, + Button, + Table, + Heading, + Pagination, + IconButton, + Checkbox, + VStack, + ButtonGroup, + Input, +} from '@chakra-ui/react'; +import { PantryStats, TotalStats } from 'types/types'; +import ApiClient from '@api/apiClient'; +import { FloatingAlert } from '@components/floatingAlert'; +import { useAlert } from '../hooks/alert'; + +const AdminDonationStats: React.FC = () => { + const [pantryStats, setPantryStats] = useState([]); + const [totalStats, setTotalStats] = useState(null); + const [pantryNameOptions, setPantryNameOptions] = useState([]); + const [selectedPantries, setSelectedPantries] = useState([]); + const [searchPantry, setSearchPantry] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [isFilterOpen, setIsFilterOpen] = useState(false); + + const [alertState, setAlertMessage] = useAlert(); + + useEffect(() => { + Promise.all([ApiClient.getApprovedPantryNames(), ApiClient.getTotalStats()]) + .then(([names, stats]) => { + setPantryNameOptions(names); + setTotalStats(stats); + }) + .catch(() => setAlertMessage('Error fetching pantry data')); + }, [setAlertMessage]); + + useEffect(() => { + const fetchStats = async () => { + try { + const stats = await ApiClient.getPantryStats({ + pantryNames: selectedPantries.length ? selectedPantries : undefined, + page: currentPage, + }); + setPantryStats(stats); + } catch { + setAlertMessage('Error fetching pantry stats'); + } + }; + fetchStats(); + }, [selectedPantries, currentPage, setAlertMessage]); + + const handleFilterChange = (name: string, checked: boolean) => { + setCurrentPage(1); + if (checked) { + setSelectedPantries([...selectedPantries, name]); + } else { + setSelectedPantries(selectedPantries.filter((n) => n !== name)); + } + }; + + const itemsPerPage = 10; + const totalCount = + selectedPantries.length > 0 + ? selectedPantries.length + : pantryNameOptions.length; + const totalPages = Math.ceil(totalCount / itemsPerPage); + + const tableHeaderStyles = { + borderBottom: '1px solid', + borderColor: 'neutral.100', + color: 'neutral.800', + fontFamily: 'inter', + fontWeight: '600', + fontSize: 'sm', + }; + + return ( + + + Donation Statistics + + {alertState && ( + + )} + + + + + {isFilterOpen && ( + <> + setIsFilterOpen(false)} + zIndex={10} + /> + + + + setSearchPantry(e.target.value)} + fontSize="sm" + pl="30px" + border="none" + bg="transparent" + _focus={{ + boxShadow: 'none', + border: 'none', + outline: 'none', + }} + /> + + + {pantryNameOptions + .filter((name) => + name.toLowerCase().includes(searchPantry.toLowerCase()), + ) + .map((name) => ( + + handleFilterChange(name, !!e.checked) + } + size="md" + > + + + {name} + + ))} + + + + )} + + + + + + + Pantry + + + Total Items + + + Total Weight (oz) + + + Total Weight (lbs) + + + Value of Donated Food + + + Shipping Cost/Tax + + + Total Value + + + + + {totalStats && ( + + + All Pantries + + + {totalStats.totalItems} + + + {totalStats.totalOz} + + + {totalStats.totalLbs} + + + ${totalStats.totalDonatedFoodValue.toFixed(2)} + + + ${totalStats.totalShippingCost.toFixed(2)} + + + ${totalStats.totalValue.toFixed(2)} + + + )} + {pantryStats.map((stat) => ( + + + {stat.pantryName} + + + {stat.totalItems} + + + {stat.totalOz} + + + {stat.totalLbs} + + + ${stat.totalDonatedFoodValue.toFixed(2)} + + + ${stat.totalShippingCost.toFixed(2)} + + + ${stat.totalValue.toFixed(2)} + + + ))} + + + + {totalPages > 1 && ( + setCurrentPage(e.page)} + > + + + + + + ( + + {page.value} + + )} + /> + + + + + + + )} + + ); +}; + +export default AdminDonationStats; diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index 3af4b1a5..777541e9 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -138,6 +138,13 @@ const Homepage: React.FC = () => { + + + + Donation Stats Management + + + diff --git a/apps/frontend/src/theme.ts b/apps/frontend/src/theme.ts index 0b320004..3ec1d691 100644 --- a/apps/frontend/src/theme.ts +++ b/apps/frontend/src/theme.ts @@ -80,6 +80,7 @@ const customConfig = defineConfig({ yellow: { ssf: { value: '#F89E19' }, hover: { value: '#9C5D00' }, + 100: { value: '#FEF5E8' }, 200: { value: '#FEECD1' }, }, cyan: { value: '#2795A5' }, diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 40a9ff21..d2f76b5f 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -359,6 +359,7 @@ export type RepeatOnState = Record; export interface PantryStats { pantryId: number; + pantryName: string; totalItems: number; totalOz: number; totalLbs: number; @@ -368,8 +369,7 @@ export interface PantryStats { percentageFoodRescueItems: number; } -// Make TotalStats interface just not include pantryId -export type TotalStats = Omit; +export type TotalStats = Omit; export type Assignments = Omit & { pantryIds: number[] }; From b4ffbb87e465a7d71079da3e710346a17b7d7429 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 22 Mar 2026 17:47:30 -0400 Subject: [PATCH 2/6] Working version precleanup --- .claude/settings.local.json | 7 + .../src/pantries/pantries.controller.spec.ts | 21 +++ .../src/pantries/pantries.controller.ts | 6 + .../src/pantries/pantries.service.spec.ts | 52 ++++++ apps/backend/src/pantries/pantries.service.ts | 26 +++ apps/frontend/src/api/apiClient.ts | 16 +- .../src/containers/adminDonationStats.tsx | 148 +++++++++++++++++- 7 files changed, 267 insertions(+), 9 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..cbd3cfd1 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(xargs grep:*)" + ] + } +} diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index 2ef6be61..17b240b4 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -108,6 +108,27 @@ describe('PantriesController', () => { expect(controller).toBeDefined(); }); + describe('getAvailableYears', () => { + it('should return an array of years', async () => { + const mockYears = [2025, 2024]; + mockPantriesService.getAvailableYears.mockResolvedValueOnce(mockYears); + + const result = await controller.getAvailableYears(); + + expect(result).toEqual(mockYears); + expect(mockPantriesService.getAvailableYears).toHaveBeenCalled(); + }); + + it('should return an empty array when no approved pantry orders exist', async () => { + mockPantriesService.getAvailableYears.mockResolvedValueOnce([]); + + const result = await controller.getAvailableYears(); + + expect(result).toEqual([]); + expect(mockPantriesService.getAvailableYears).toHaveBeenCalled(); + }); + }); + describe('getApprovedPantryNames', () => { it('should return an array of approved pantry names', async () => { const mockNames = ['Pantry A', 'Pantry B']; diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index 83628aa3..a194e390 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -81,6 +81,12 @@ export class PantriesController { return this.pantriesService.getApprovedPantryNames(); } + @Roles(Role.ADMIN) + @Get('/available-years') + async getAvailableYears(): Promise { + return this.pantriesService.getAvailableYears(); + } + @CheckOwnership({ idParam: 'pantryId', resolver: async ({ entityId, services }) => { diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts index a65ed1df..26e1c0c7 100644 --- a/apps/backend/src/pantries/pantries.service.spec.ts +++ b/apps/backend/src/pantries/pantries.service.spec.ts @@ -602,6 +602,58 @@ describe('PantriesService', () => { }); }); + describe('getAvailableYears', () => { + it('returns years from approved pantry orders sorted descending', async () => { + await testDataSource.query( + `UPDATE public.orders SET created_at = '2024-06-01 00:00:00'`, + ); + + const years = await service.getAvailableYears(); + + expect(years).toEqual([2024]); + }); + + it('returns multiple years sorted descending', async () => { + await testDataSource.query(` + UPDATE public.orders + SET created_at = '2025-01-01 00:00:00' + WHERE order_id = (SELECT order_id FROM public.orders ORDER BY order_id LIMIT 1) + `); + await testDataSource.query(` + UPDATE public.orders + SET created_at = '2024-01-01 00:00:00' + WHERE order_id != (SELECT order_id FROM public.orders ORDER BY order_id LIMIT 1) + `); + + const years = await service.getAvailableYears(); + + expect(years).toEqual([2025, 2024]); + }); + + it('returns empty array when no approved pantries exist', async () => { + await testDataSource.query( + `UPDATE public.pantries SET status = 'pending' WHERE status = 'approved'`, + ); + + const years = await service.getAvailableYears(); + + expect(years).toEqual([]); + }); + + it('excludes years from non-approved pantry orders', async () => { + await testDataSource.query( + `UPDATE public.orders SET created_at = '2024-06-01 00:00:00'`, + ); + await testDataSource.query( + `UPDATE public.pantries SET status = 'pending' WHERE status = 'approved'`, + ); + + const years = await service.getAvailableYears(); + + expect(years).toEqual([]); + }); + }); + describe('findByIds', () => { it('findByIds success', async () => { const found = await service.findByIds([1, 2]); diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts index 5796b332..6cc0c210 100644 --- a/apps/backend/src/pantries/pantries.service.ts +++ b/apps/backend/src/pantries/pantries.service.ts @@ -286,6 +286,32 @@ export class PantriesService { return pantries.map((p) => p.pantryName); } + async getAvailableYears(): Promise { + const approvedPantries = await this.repo.find({ + select: ['pantryId'], + where: { status: ApplicationStatus.APPROVED }, + }); + + if (approvedPantries.length === 0) { + return []; + } + + const approvedPantryIds = approvedPantries.map((p) => p.pantryId); + + const rows = await this.orderRepo + .createQueryBuilder('order') + .leftJoin('order.request', 'request') + .select('EXTRACT(YEAR FROM order.createdAt)::int', 'year') + .where('request.pantryId IN (:...pantryIds)', { + pantryIds: approvedPantryIds, + }) + .groupBy('EXTRACT(YEAR FROM order.createdAt)::int') + .orderBy('"year"', 'DESC') + .getRawMany(); + + return rows.map((r) => Number(r.year)); + } + async addPantry(pantryData: PantryApplicationDto) { const pantryContact: User = new User(); const pantry: Pantry = new Pantry(); diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 14febb79..1ee4a4c4 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -183,6 +183,7 @@ export class ApiClient { public async getPantryStats(params?: { pantryNames?: string[]; + years?: number[]; page?: number; }): Promise { return this.axiosInstance @@ -191,15 +192,20 @@ export class ApiClient { ...(params?.pantryNames?.length && { pantryNames: params.pantryNames, }), + ...(params?.years?.length && { years: params.years }), ...(params?.page && { page: params.page }), }, }) .then((response) => response.data); } - public async getTotalStats(): Promise { + public async getTotalStats(years?: number[]): Promise { return this.axiosInstance - .get('/api/pantries/total-stats') + .get('/api/pantries/total-stats', { + params: { + ...(years?.length && { years }), + }, + }) .then((response) => response.data); } @@ -209,6 +215,12 @@ export class ApiClient { .then((response) => response.data); } + public async getAvailableYears(): Promise { + return this.axiosInstance + .get('/api/pantries/available-years') + .then((response) => response.data); + } + public async getFoodRequestFromOrder( orderId: number, ): Promise { diff --git a/apps/frontend/src/containers/adminDonationStats.tsx b/apps/frontend/src/containers/adminDonationStats.tsx index eda6b22e..fd143cbf 100644 --- a/apps/frontend/src/containers/adminDonationStats.tsx +++ b/apps/frontend/src/containers/adminDonationStats.tsx @@ -1,8 +1,8 @@ import React, { useState, useEffect } from 'react'; import { - ArrowDownUp, ChevronRight, ChevronLeft, + ChevronDown, Funnel, Search, } from 'lucide-react'; @@ -27,27 +27,47 @@ const AdminDonationStats: React.FC = () => { const [pantryStats, setPantryStats] = useState([]); const [totalStats, setTotalStats] = useState(null); const [pantryNameOptions, setPantryNameOptions] = useState([]); + const [availableYears, setAvailableYears] = useState([]); const [selectedPantries, setSelectedPantries] = useState([]); + const [selectedYears, setSelectedYears] = useState([]); const [searchPantry, setSearchPantry] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [isFilterOpen, setIsFilterOpen] = useState(false); + const [isYearFilterOpen, setIsYearFilterOpen] = useState(false); const [alertState, setAlertMessage] = useAlert(); useEffect(() => { - Promise.all([ApiClient.getApprovedPantryNames(), ApiClient.getTotalStats()]) - .then(([names, stats]) => { + Promise.all([ + ApiClient.getApprovedPantryNames(), + ApiClient.getAvailableYears(), + ]) + .then(([names, years]) => { setPantryNameOptions(names); - setTotalStats(stats); + setAvailableYears(years); + setSelectedYears(years); }) .catch(() => setAlertMessage('Error fetching pantry data')); }, [setAlertMessage]); + useEffect(() => { + const allSelected = + selectedYears.length === 0 || + selectedYears.length === availableYears.length; + ApiClient.getTotalStats(allSelected ? undefined : selectedYears) + .then(setTotalStats) + .catch(() => setAlertMessage('Error fetching total stats')); + }, [selectedYears, availableYears, setAlertMessage]); + useEffect(() => { const fetchStats = async () => { try { + const allSelected = + selectedYears.length === 0 || + selectedYears.length === availableYears.length; const stats = await ApiClient.getPantryStats({ pantryNames: selectedPantries.length ? selectedPantries : undefined, + years: allSelected ? undefined : selectedYears, page: currentPage, }); setPantryStats(stats); @@ -56,7 +76,13 @@ const AdminDonationStats: React.FC = () => { } }; fetchStats(); - }, [selectedPantries, currentPage, setAlertMessage]); + }, [ + selectedPantries, + selectedYears, + availableYears, + currentPage, + setAlertMessage, + ]); const handleFilterChange = (name: string, checked: boolean) => { setCurrentPage(1); @@ -67,6 +93,22 @@ const AdminDonationStats: React.FC = () => { } }; + const handleYearFilterChange = (year: number, checked: boolean) => { + setCurrentPage(1); + if (checked) { + setSelectedYears([...selectedYears, year]); + } else { + setSelectedYears(selectedYears.filter((y) => y !== year)); + } + }; + + const allYearsSelected = + availableYears.length > 0 && selectedYears.length === availableYears.length; + const yearButtonLabel = + allYearsSelected || selectedYears.length === 0 + ? 'All Available Years' + : [...selectedYears].sort((a, b) => a - b).join(', '); + const itemsPerPage = 10; const totalCount = selectedPantries.length > 0 @@ -97,11 +139,11 @@ const AdminDonationStats: React.FC = () => { /> )} - + + + {isYearFilterOpen && ( + <> + setIsYearFilterOpen(false)} + zIndex={10} + /> + + + { + setCurrentPage(1); + setSelectedYears(e.checked ? [...availableYears] : []); + }} + size="md" + > + + + All Available Years + + {[...availableYears] + .sort((a, b) => a - b) + .map((year) => ( + + handleYearFilterChange(year, !!e.checked) + } + size="md" + > + + + {year} + + ))} + + + + )} + From 2c655d0e4c45eefaac8634da5ae0d36c60a109a6 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 22 Mar 2026 18:16:26 -0400 Subject: [PATCH 3/6] final commit --- .../src/containers/adminDonationStats.tsx | 84 ++++++++++--------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/apps/frontend/src/containers/adminDonationStats.tsx b/apps/frontend/src/containers/adminDonationStats.tsx index fd143cbf..d2b2c528 100644 --- a/apps/frontend/src/containers/adminDonationStats.tsx +++ b/apps/frontend/src/containers/adminDonationStats.tsx @@ -24,10 +24,13 @@ import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../hooks/alert'; const AdminDonationStats: React.FC = () => { + // Individual and combined pantry stats to be displayed const [pantryStats, setPantryStats] = useState([]); - const [totalStats, setTotalStats] = useState(null); + const [totalStats, setTotalStats] = useState(); + // Names and years of all approved pantries, used for filters const [pantryNameOptions, setPantryNameOptions] = useState([]); const [availableYears, setAvailableYears] = useState([]); + // Filtering state management const [selectedPantries, setSelectedPantries] = useState([]); const [selectedYears, setSelectedYears] = useState([]); const [searchPantry, setSearchPantry] = useState(''); @@ -38,26 +41,29 @@ const AdminDonationStats: React.FC = () => { const [alertState, setAlertMessage] = useAlert(); useEffect(() => { - Promise.all([ - ApiClient.getApprovedPantryNames(), - ApiClient.getAvailableYears(), - ]) - .then(([names, years]) => { - setPantryNameOptions(names); + ApiClient.getApprovedPantryNames() + .then(setPantryNameOptions) + .catch(() => setAlertMessage('Error fetching pantry names')); + + ApiClient.getAvailableYears() + .then((years) => { setAvailableYears(years); + // On page load, set all years to selected setSelectedYears(years); }) - .catch(() => setAlertMessage('Error fetching pantry data')); - }, [setAlertMessage]); + .catch(() => setAlertMessage('Error fetching available years')); + }, []); useEffect(() => { + // Default show all selected const allSelected = selectedYears.length === 0 || selectedYears.length === availableYears.length; + ApiClient.getTotalStats(allSelected ? undefined : selectedYears) .then(setTotalStats) .catch(() => setAlertMessage('Error fetching total stats')); - }, [selectedYears, availableYears, setAlertMessage]); + }, [selectedYears, availableYears]); useEffect(() => { const fetchStats = async () => { @@ -76,15 +82,10 @@ const AdminDonationStats: React.FC = () => { } }; fetchStats(); - }, [ - selectedPantries, - selectedYears, - availableYears, - currentPage, - setAlertMessage, - ]); + }, [selectedPantries, selectedYears, availableYears, currentPage]); - const handleFilterChange = (name: string, checked: boolean) => { + const handlePantryNameFilterChange = (name: string, checked: boolean) => { + // For simplicity, reset the page setCurrentPage(1); if (checked) { setSelectedPantries([...selectedPantries, name]); @@ -94,6 +95,7 @@ const AdminDonationStats: React.FC = () => { }; const handleYearFilterChange = (year: number, checked: boolean) => { + // For simplicity, reset the page setCurrentPage(1); if (checked) { setSelectedYears([...selectedYears, year]); @@ -104,9 +106,11 @@ const AdminDonationStats: React.FC = () => { const allYearsSelected = availableYears.length > 0 && selectedYears.length === availableYears.length; + + // Default All Available Data, otherwise display as many years as possible in ascending order const yearButtonLabel = allYearsSelected || selectedYears.length === 0 - ? 'All Available Years' + ? 'All Available Data' : [...selectedYears].sort((a, b) => a - b).join(', '); const itemsPerPage = 10; @@ -226,7 +230,7 @@ const AdminDonationStats: React.FC = () => { key={name} checked={selectedPantries.includes(name)} onCheckedChange={(e: { checked: boolean }) => - handleFilterChange(name, !!e.checked) + handlePantryNameFilterChange(name, !!e.checked) } size="md" > @@ -309,24 +313,22 @@ const AdminDonationStats: React.FC = () => { > - All Available Years + All Available Data - {[...availableYears] - .sort((a, b) => a - b) - .map((year) => ( - - handleYearFilterChange(year, !!e.checked) - } - size="md" - > - - - {year} - - ))} + {[...availableYears].map((year) => ( + + handleYearFilterChange(year, !!e.checked) + } + size="md" + > + + + {year} + + ))} @@ -414,7 +416,7 @@ const AdminDonationStats: React.FC = () => { borderRightColor="neutral.100" bg="yellow.100" > - {totalStats.totalOz} + {totalStats.totalOz.toFixed(2)} { borderRightColor="neutral.100" bg="yellow.100" > - {totalStats.totalLbs} + {totalStats.totalLbs.toFixed(2)} { borderRight="1px solid" borderRightColor="neutral.100" > - {stat.totalOz} + {stat.totalOz.toFixed(2)} - {stat.totalLbs} + {stat.totalLbs.toFixed(2)} Date: Sun, 22 Mar 2026 18:20:35 -0400 Subject: [PATCH 4/6] small fixes --- apps/backend/src/users/users.controller.spec.ts | 1 - apps/backend/src/users/users.controller.ts | 1 - .../src/components/forms/deliveryConfirmationModal.tsx | 1 - apps/frontend/src/components/forms/newDonationFormModal.tsx | 3 --- 4 files changed, 6 deletions(-) diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 3fd20753..6c369474 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -6,7 +6,6 @@ import { userSchemaDto } from './dtos/userSchema.dto'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; import { UpdateUserInfoDto } from './dtos/update-user-info.dto'; -import { Pantry } from '../pantries/pantries.entity'; import { BadRequestException } from '@nestjs/common'; import { AuthenticatedRequest } from '../auth/authenticated-request'; diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index bd05e123..d699e2ad 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -8,7 +8,6 @@ import { Body, Patch, Req, - ValidationPipe, } from '@nestjs/common'; import { UsersService } from './users.service'; import { User } from './users.entity'; diff --git a/apps/frontend/src/components/forms/deliveryConfirmationModal.tsx b/apps/frontend/src/components/forms/deliveryConfirmationModal.tsx index a0bb284d..77ef34cc 100644 --- a/apps/frontend/src/components/forms/deliveryConfirmationModal.tsx +++ b/apps/frontend/src/components/forms/deliveryConfirmationModal.tsx @@ -156,7 +156,6 @@ export const submitDeliveryConfirmationFormModal: ActionFunction = async ({ const form = await request.formData(); const confirmDeliveryData = new FormData(); - const pantryId = form.get('pantryId') as string; const requestId = form.get('requestId') as string; confirmDeliveryData.append('requestId', requestId); diff --git a/apps/frontend/src/components/forms/newDonationFormModal.tsx b/apps/frontend/src/components/forms/newDonationFormModal.tsx index f4cf7df7..b3fee374 100644 --- a/apps/frontend/src/components/forms/newDonationFormModal.tsx +++ b/apps/frontend/src/components/forms/newDonationFormModal.tsx @@ -85,9 +85,6 @@ const NewDonationFormModal: React.FC = ({ }); const [endsAfter, setEndsAfter] = useState('1'); - const [totalItems, setTotalItems] = useState(0); - const [totalOz, setTotalOz] = useState(0); - const [totalValue, setTotalValue] = useState(0); const [alertState, setAlertMessage] = useAlert(); const handleChange = (id: number, field: string, value: string | boolean) => { From eeb9ba702d569f623c35cc80a731d724061fc8c8 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 22 Mar 2026 18:25:51 -0400 Subject: [PATCH 5/6] final commit --- .claude/settings.local.json | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index cbd3cfd1..00000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(xargs grep:*)" - ] - } -} From a33dd70ccd1e9dfee1c0c022623add0d4d231345 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Tue, 24 Mar 2026 20:17:52 -0400 Subject: [PATCH 6/6] Comments --- .../src/pantries/pantries.controller.spec.ts | 15 --------------- .../components/forms/requestDetailsModal.tsx | 6 +++++- apps/frontend/src/containers/adminDonation.tsx | 5 +++++ .../src/containers/adminDonationStats.tsx | 18 ++++++++++++++---- .../src/containers/adminOrderManagement.tsx | 3 +++ .../src/containers/approvePantries.tsx | 5 +++++ .../foodManufacturerDonationManagement.tsx | 3 +++ apps/frontend/src/containers/formRequests.tsx | 6 +++++- apps/frontend/src/containers/homepage.tsx | 4 ++-- .../src/containers/pantryOrderManagement.tsx | 3 +++ .../src/containers/volunteerManagement.tsx | 9 ++++++++- .../containers/volunteerRequestManagement.tsx | 5 +++++ 12 files changed, 58 insertions(+), 24 deletions(-) diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index 96c60c16..ef3f59d9 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -471,21 +471,6 @@ describe('PantriesController', () => { page, ); }); - - it('should propagate NotFoundException when a non-approved pantry name is queried', async () => { - mockPantriesService.getPantryStats.mockRejectedValueOnce( - new Error('Pantries not found: Riverside Food Assistance'), - ); - - await expect( - controller.getPantryStats(['Riverside Food Assistance']), - ).rejects.toThrow('Pantries not found: Riverside Food Assistance'); - expect(mockPantriesService.getPantryStats).toHaveBeenCalledWith( - ['Riverside Food Assistance'], - undefined, - 1, - ); - }); }); describe('getTotalStats', () => { diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index ac719d99..2adc9509 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -278,13 +278,15 @@ const RequestDetailsModal: React.FC = ({ page={currentPage} onChange={(page: number) => setCurrentPage(page)} > - + setCurrentPage((prev) => Math.max(prev - 1, 1)) } + ml={2} > @@ -305,11 +307,13 @@ const RequestDetailsModal: React.FC = ({ setCurrentPage((prev) => Math.min(prev + 1, orderDetailsList.length), ) } + mr={2} > diff --git a/apps/frontend/src/containers/adminDonation.tsx b/apps/frontend/src/containers/adminDonation.tsx index 1c159528..fd3479ac 100644 --- a/apps/frontend/src/containers/adminDonation.tsx +++ b/apps/frontend/src/containers/adminDonation.tsx @@ -276,11 +276,14 @@ const AdminDonation: React.FC = () => { mt={12} variant="outline" size="sm" + gap={2} > @@ -301,7 +304,9 @@ const AdminDonation: React.FC = () => { diff --git a/apps/frontend/src/containers/adminDonationStats.tsx b/apps/frontend/src/containers/adminDonationStats.tsx index d2b2c528..fc1c8475 100644 --- a/apps/frontend/src/containers/adminDonationStats.tsx +++ b/apps/frontend/src/containers/adminDonationStats.tsx @@ -55,7 +55,9 @@ const AdminDonationStats: React.FC = () => { }, []); useEffect(() => { - // Default show all selected + // Total stats only displayed on first page, so no need to do anything on page change + if (currentPage !== 1) return; + const allSelected = selectedYears.length === 0 || selectedYears.length === availableYears.length; @@ -63,7 +65,7 @@ const AdminDonationStats: React.FC = () => { ApiClient.getTotalStats(allSelected ? undefined : selectedYears) .then(setTotalStats) .catch(() => setAlertMessage('Error fetching total stats')); - }, [selectedYears, availableYears]); + }, [selectedYears, availableYears, currentPage]); useEffect(() => { const fetchStats = async () => { @@ -119,6 +121,9 @@ const AdminDonationStats: React.FC = () => { ? selectedPantries.length : pantryNameOptions.length; const totalPages = Math.ceil(totalCount / itemsPerPage); + // 9 pantries for the first page + const displayedStats = + currentPage === 1 ? pantryStats.slice(0, 9) : pantryStats; const tableHeaderStyles = { borderBottom: '1px solid', @@ -392,7 +397,7 @@ const AdminDonationStats: React.FC = () => { - {totalStats && ( + {currentPage === 1 && totalStats && ( { )} - {pantryStats.map((stat) => ( + {displayedStats.map((stat) => ( { mt={12} variant="outline" size="sm" + gap={4} > @@ -539,6 +547,8 @@ const AdminDonationStats: React.FC = () => { diff --git a/apps/frontend/src/containers/adminOrderManagement.tsx b/apps/frontend/src/containers/adminOrderManagement.tsx index cbec3241..bb49cf8e 100644 --- a/apps/frontend/src/containers/adminOrderManagement.tsx +++ b/apps/frontend/src/containers/adminOrderManagement.tsx @@ -739,11 +739,13 @@ const OrderStatusSection: React.FC = ({ alignItems="center" variant="outline" size="sm" + gap={2} > = ({ color="neutral.800" _hover={{ color: 'black' }} disabled={currentPage === totalPages} + mr={2} > { mt={12} variant="outline" size="sm" + gap={2} > @@ -357,7 +360,9 @@ const ApprovePantries: React.FC = () => { diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx index d295e2c5..138507cb 100644 --- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx +++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx @@ -353,11 +353,13 @@ const DonationStatusSection: React.FC = ({ alignItems="center" variant="outline" size="sm" + gap={2} > = ({ color="neutral.800" _hover={{ color: 'black' }} disabled={currentPage === totalPages} + mr={2} > { page={currentPage} onChange={(page: number) => setCurrentPage(page)} > - + setCurrentPage((prev) => Math.max(prev - 1, 1))} + ml={2} > @@ -243,11 +245,13 @@ const FormRequests: React.FC = () => { setCurrentPage((prev) => Math.min(prev + 1, Math.ceil(requests.length / pageSize)), ) } + mr={2} > diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx index ec0ffc59..ff7d720f 100644 --- a/apps/frontend/src/containers/homepage.tsx +++ b/apps/frontend/src/containers/homepage.tsx @@ -152,8 +152,8 @@ const Homepage: React.FC = () => { - - Donation Stats Management + + Donation Statistics diff --git a/apps/frontend/src/containers/pantryOrderManagement.tsx b/apps/frontend/src/containers/pantryOrderManagement.tsx index 1b03cb35..82c4d3e9 100644 --- a/apps/frontend/src/containers/pantryOrderManagement.tsx +++ b/apps/frontend/src/containers/pantryOrderManagement.tsx @@ -546,11 +546,13 @@ const OrderStatusSection: React.FC = ({ alignItems="center" variant="outline" size="sm" + gap={2} > = ({ color="neutral.800" _hover={{ color: 'black' }} disabled={currentPage === totalPages} + mr={2} > { page={currentPage} onChange={(page: number) => setCurrentPage(page)} > - + setCurrentPage((prev) => Math.max(prev - 1, 1)) } + ml={2} > @@ -209,6 +211,7 @@ const VolunteerManagement: React.FC = () => { setCurrentPage(page.value)} + mr={2} > {page.value} @@ -218,6 +221,10 @@ const VolunteerManagement: React.FC = () => { setCurrentPage((prev) => Math.min( diff --git a/apps/frontend/src/containers/volunteerRequestManagement.tsx b/apps/frontend/src/containers/volunteerRequestManagement.tsx index dace2d42..75a45a29 100644 --- a/apps/frontend/src/containers/volunteerRequestManagement.tsx +++ b/apps/frontend/src/containers/volunteerRequestManagement.tsx @@ -324,11 +324,14 @@ const VolunteerRequestManagement: React.FC = () => { mt={12} variant="outline" size="sm" + gap={2} > @@ -349,7 +352,9 @@ const VolunteerRequestManagement: React.FC = () => {