diff --git a/.gitignore b/.gitignore index 0d46e7cf..b4fe935f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,7 @@ npm-debug.log yarn-error.log testem.log /typings -.nx - + # System Files .DS_Store Thumbs.db diff --git a/apps/backend/src/pantries/dtos/pantry-application.dto.ts b/apps/backend/src/pantries/dtos/pantry-application.dto.ts index c7473b0f..6a607eda 100644 --- a/apps/backend/src/pantries/dtos/pantry-application.dto.ts +++ b/apps/backend/src/pantries/dtos/pantry-application.dto.ts @@ -220,4 +220,4 @@ export class PantryApplicationDto { @IsOptional() @IsBoolean() newsletterSubscription?: boolean; -} +} \ No newline at end of file diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index 3d58f84c..e1fc4d25 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -4,6 +4,15 @@ import { PantriesController } from './pantries.controller'; import { PantriesService } from './pantries.service'; import { OrdersService } from '../orders/order.service'; import { Order } from '../orders/order.entity'; +import { + Activity, + AllergensConfidence, + ApprovedPantryResponse, + ClientVisitFrequency, + ReserveFoodForAllergic, + ServeAllergicChildren, +} from './types'; +import { RefrigeratedDonation } from './types'; const mockPantriesService = mock(); const mockOrdersService = mock(); @@ -59,4 +68,77 @@ describe('PantriesController', () => { expect(mockOrdersService.getOrdersByPantry).toHaveBeenCalledWith(24); }); }); + + describe('getApprovedPantries', () => { + it('should return approved pantries with volunteers', async () => { + const mockApprovedPantries: ApprovedPantryResponse[] = [ + { + pantryId: 1, + pantryName: 'Community Food Pantry', + + contactFirstName: 'John', + contactLastName: 'Smith', + contactEmail: 'john.smith@example.com', + contactPhone: '(508) 508-6789', + + shipmentAddressLine1: '123 Main Street', + shipmentAddressCity: 'Boston', + shipmentAddressZip: '02101', + shipmentAddressCountry: 'United States', + + allergenClients: '10 to 20', + restrictions: ['Peanuts', 'Dairy'], + + refrigeratedDonation: RefrigeratedDonation.YES, + reserveFoodForAllergic: ReserveFoodForAllergic.YES, + reservationExplanation: + 'We regularly serve clients with severe allergies.', + + dedicatedAllergyFriendly: true, + + clientVisitFrequency: ClientVisitFrequency.FEW_TIMES_A_MONTH, + identifyAllergensConfidence: AllergensConfidence.VERY_CONFIDENT, + serveAllergicChildren: ServeAllergicChildren.YES_MANY, + + activities: [ + Activity.POST_RESOURCE_FLYERS, + Activity.CREATE_LABELED_SHELF, + ], + activitiesComments: 'Weekly food distribution events', + + itemsInStock: 'Canned goods, rice, pasta', + needMoreOptions: 'Gluten-free and nut-free items', + + newsletterSubscription: true, + }, + ]; + + mockPantriesService.getApprovedPantriesWithVolunteers.mockResolvedValue( + mockApprovedPantries, + ); + + const result = await controller.getApprovedPantries(); + + expect(result).toEqual(mockApprovedPantries); + expect( + mockPantriesService.getApprovedPantriesWithVolunteers, + ).toHaveBeenCalledTimes(1); + }); + }); + + describe('updatePantryVolunteers', () => { + it('should overwrite the set of volunteers assigned to a pantry', async () => { + const pantryId = 1; + const volunteerIds = [10, 11, 12]; + + mockPantriesService.updatePantryVolunteers.mockResolvedValue(undefined); + + await controller.updatePantryVolunteers(pantryId, volunteerIds); + + expect(mockPantriesService.updatePantryVolunteers).toHaveBeenCalledWith( + pantryId, + volunteerIds, + ); + }); + }); }); diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index ee8287ce..504e7d49 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -5,12 +5,14 @@ import { Param, ParseIntPipe, Post, + Put, ValidationPipe, } from '@nestjs/common'; import { Pantry } from './pantries.entity'; import { PantriesService } from './pantries.service'; import { PantryApplicationDto } from './dtos/pantry-application.dto'; import { ApiBody } from '@nestjs/swagger'; +import { ApprovedPantryResponse } from './types'; import { Activity, AllergensConfidence, @@ -34,6 +36,11 @@ export class PantriesController { return this.pantriesService.getPendingPantries(); } + @Get('/approved') + async getApprovedPantries(): Promise { + return this.pantriesService.getApprovedPantriesWithVolunteers(); + } + @Get('/:pantryId') async getPantry( @Param('pantryId', ParseIntPipe) pantryId: number, @@ -225,4 +232,12 @@ export class PantriesController { ): Promise { return this.pantriesService.deny(pantryId); } + + @Put('/:pantryId/volunteers') + async updatePantryVolunteers( + @Param('pantryId', ParseIntPipe) pantryId: number, + @Body() volunteerIds: number[], + ): Promise { + return this.pantriesService.updatePantryVolunteers(pantryId, volunteerIds); + } } diff --git a/apps/backend/src/pantries/pantries.entity.ts b/apps/backend/src/pantries/pantries.entity.ts index 2ad5c4ff..353d1f5e 100644 --- a/apps/backend/src/pantries/pantries.entity.ts +++ b/apps/backend/src/pantries/pantries.entity.ts @@ -108,7 +108,7 @@ export class Pantry { enum: ReserveFoodForAllergic, enumName: 'reserve_food_for_allergic_enum', }) - reserveFoodForAllergic: string; + reserveFoodForAllergic: ReserveFoodForAllergic; @Column({ name: 'reservation_explanation', type: 'text', nullable: true }) reservationExplanation?: string; diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts index a2f254ec..ac4939c5 100644 --- a/apps/backend/src/pantries/pantries.service.ts +++ b/apps/backend/src/pantries/pantries.service.ts @@ -7,6 +7,7 @@ import { validateId } from '../utils/validation.utils'; import { PantryStatus } from './types'; import { PantryApplicationDto } from './dtos/pantry-application.dto'; import { Role } from '../users/types'; +import { ApprovedPantryResponse } from './types'; @Injectable() export class PantriesService { @@ -110,6 +111,61 @@ export class PantriesService { await this.repo.update(id, { status: PantryStatus.DENIED }); } + async getApprovedPantriesWithVolunteers(): Promise { + const pantries = await this.repo.find({ + where: { status: PantryStatus.APPROVED }, + relations: ['pantryUser'], + }); + + return pantries.map((pantry) => ({ + pantryId: pantry.pantryId, + pantryName: pantry.pantryName, + contactFirstName: pantry.pantryUser.firstName, + contactLastName: pantry.pantryUser.lastName, + contactEmail: pantry.pantryUser.email, + contactPhone: pantry.pantryUser.phone, + shipmentAddressLine1: pantry.shipmentAddressLine1, + shipmentAddressCity: pantry.shipmentAddressCity, + shipmentAddressZip: pantry.shipmentAddressZip, + shipmentAddressCountry: pantry.shipmentAddressCountry, + allergenClients: pantry.allergenClients, + restrictions: pantry.restrictions, + refrigeratedDonation: pantry.refrigeratedDonation, + reserveFoodForAllergic: pantry.reserveFoodForAllergic, + reservationExplanation: pantry.reservationExplanation, + dedicatedAllergyFriendly: pantry.dedicatedAllergyFriendly, + clientVisitFrequency: pantry.clientVisitFrequency, + identifyAllergensConfidence: pantry.identifyAllergensConfidence, + serveAllergicChildren: pantry.serveAllergicChildren, + activities: pantry.activities, + activitiesComments: pantry.activitiesComments, + itemsInStock: pantry.itemsInStock, + needMoreOptions: pantry.needMoreOptions, + newsletterSubscription: pantry.newsletterSubscription ?? false, + })); + } + + async updatePantryVolunteers( + pantryId: number, + volunteerIds: number[], + ): Promise { + validateId(pantryId, 'Pantry'); + volunteerIds.forEach((id) => validateId(id, 'Volunteer')); + + const pantry = await this.repo.findOne({ + where: { pantryId }, + relations: ['volunteers'], + }); + + if (!pantry) { + throw new NotFoundException(`Pantry with ID ${pantryId} not found`); + } + + pantry.volunteers = volunteerIds.map((id) => ({ id } as User)); + + await this.repo.save(pantry); + } + async findByIds(pantryIds: number[]): Promise { pantryIds.forEach((id) => validateId(id, 'Pantry')); diff --git a/apps/backend/src/pantries/types.ts b/apps/backend/src/pantries/types.ts index cdf8b671..c0197c7e 100644 --- a/apps/backend/src/pantries/types.ts +++ b/apps/backend/src/pantries/types.ts @@ -1,3 +1,46 @@ +export interface ApprovedPantryResponse { + pantryId: number; + pantryName: string; + contactFirstName: string; + contactLastName: string; + contactEmail: string; + contactPhone: string; + shipmentAddressLine1: string; + shipmentAddressCity: string; + shipmentAddressZip: string; + shipmentAddressCountry?: string; + allergenClients: string; + restrictions: string[]; + refrigeratedDonation: RefrigeratedDonation; + reserveFoodForAllergic: ReserveFoodForAllergic; + reservationExplanation?: string; + dedicatedAllergyFriendly: boolean; + clientVisitFrequency?: ClientVisitFrequency; + identifyAllergensConfidence?: AllergensConfidence; + serveAllergicChildren?: ServeAllergicChildren; + activities: Activity[]; + activitiesComments?: string; + itemsInStock: string; + needMoreOptions: string; + newsletterSubscription: boolean; +} + +export interface AssignedVolunteer { + userId: number; + name: string; + email: string; + phone: string; + role: string; +} + +export interface AssignedVolunteer { + userId: number; + name: string; + email: string; + phone: string; + role: string; +} + export enum RefrigeratedDonation { YES = 'Yes, always', NO = 'No', diff --git a/apps/frontend/src/components/forms/pantryApplicationForm.tsx b/apps/frontend/src/components/forms/pantryApplicationForm.tsx index df185caa..ac68ef8a 100644 --- a/apps/frontend/src/components/forms/pantryApplicationForm.tsx +++ b/apps/frontend/src/components/forms/pantryApplicationForm.tsx @@ -1252,4 +1252,4 @@ export const submitPantryApplicationForm: ActionFunction = async ({ : null; }; -export default PantryApplicationForm; +export default PantryApplicationForm; \ No newline at end of file diff --git a/apps/frontend/src/components/forms/usPhoneInput.tsx b/apps/frontend/src/components/forms/usPhoneInput.tsx index dd172c16..3d615dd3 100644 --- a/apps/frontend/src/components/forms/usPhoneInput.tsx +++ b/apps/frontend/src/components/forms/usPhoneInput.tsx @@ -69,4 +69,4 @@ export const USPhoneInput: React.FC = ({ {...inputProps} /> ); -}; +}; \ No newline at end of file diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index 7f571895..10110223 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -14,4 +14,4 @@ root.render( , -); +); \ No newline at end of file diff --git a/apps/frontend/src/theme.ts b/apps/frontend/src/theme.ts index b6312314..9dc5763c 100644 --- a/apps/frontend/src/theme.ts +++ b/apps/frontend/src/theme.ts @@ -108,4 +108,4 @@ const customConfig = defineConfig({ }, }); -export const system = createSystem(defaultConfig, customConfig); +export const system = createSystem(defaultConfig, customConfig); \ No newline at end of file