From db5428411c9db1051902bc53710d80643eb81345 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 14 Jun 2026 16:33:51 -0700 Subject: [PATCH] Final commit --- apps/backend/src/config/migrations.ts | 2 + apps/backend/src/donationItems/types.ts | 17 +- .../foodRequests/dtos/create-request.dto.ts | 9 + .../dtos/food-request-summary.dto.ts | 2 + .../foodRequests/dtos/update-request.dto.ts | 10 + .../foodRequests/request.controller.spec.ts | 4 + .../src/foodRequests/request.controller.ts | 11 ++ .../src/foodRequests/request.entity.ts | 6 + .../src/foodRequests/request.service.spec.ts | 84 +++++--- .../src/foodRequests/request.service.ts | 10 +- .../1781476891610-UpdateFoodTypesAndOrder.ts | 179 ++++++++++++++++++ apps/backend/src/orders/order.module.ts | 2 + apps/backend/src/orders/order.service.spec.ts | 4 +- apps/backend/src/orders/order.service.ts | 4 + .../src/volunteers/volunteers.service.ts | 2 + .../components/forms/requestDetailsModal.tsx | 24 +++ .../src/components/forms/requestFormModal.tsx | 117 +++++++++++- apps/frontend/src/types/types.ts | 23 ++- 18 files changed, 460 insertions(+), 50 deletions(-) create mode 100644 apps/backend/src/migrations/1781476891610-UpdateFoodTypesAndOrder.ts diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 65a88e352..416b69c39 100644 --- a/apps/backend/src/config/migrations.ts +++ b/apps/backend/src/config/migrations.ts @@ -41,6 +41,7 @@ import { MakeFoodRescueRequired1773889925002 } from '../migrations/1773889925002 import { AddDonationItemConfirmation1774140453305 } from '../migrations/1774140453305-AddDonationItemConfirmation'; import { DonationItemsOnDeleteCascade1774214910101 } from '../migrations/1774214910101-DonationItemsOnDeleteCascade'; import { OrdersVolunteerActions1774883880543 } from '../migrations/1774883880543-OrdersVolunteerActions'; +import { UpdateFoodRequestTypesAndOrder1781476891610 } from '../migrations/1781476891610-UpdateFoodTypesAndOrder'; const schemaMigrations = [ User1725726359198, @@ -86,6 +87,7 @@ const schemaMigrations = [ AddDonationItemConfirmation1774140453305, DonationItemsOnDeleteCascade1774214910101, OrdersVolunteerActions1774883880543, + UpdateFoodRequestTypesAndOrder1781476891610, ]; export default schemaMigrations; diff --git a/apps/backend/src/donationItems/types.ts b/apps/backend/src/donationItems/types.ts index 5d6497c6a..50abbfd23 100644 --- a/apps/backend/src/donationItems/types.ts +++ b/apps/backend/src/donationItems/types.ts @@ -1,16 +1,19 @@ export enum FoodType { DAIRY_FREE_ALTERNATIVES = 'Dairy-Free Alternatives', - DRIED_BEANS = 'Dried Beans (Gluten-Free, Nut-Free)', + DRIED_BEANS = 'Dried Beans', + FROZEN_MEALS = 'Frozen Meals', GLUTEN_FREE_BAKING_PANCAKE_MIXES = 'Gluten-Free Baking/Pancake Mixes', GLUTEN_FREE_BREAD = 'Gluten-Free Bread', - GLUTEN_FREE_TORTILLAS = 'Gluten-Free Tortillas', + GLUTEN_FREE_PASTA = 'Gluten-Free Pasta', + GLUTEN_FREE_TORTILLAS_FROZEN = 'Gluten-Free Tortillas (Frozen)', GRANOLA = 'Granola', + GRANOLA_BARS = 'Granola Bars', MASA_HARINA_FLOUR = 'Masa Harina Flour', - NUT_FREE_GRANOLA_BARS = 'Nut-Free Granola Bars', + NON_GMO_COOKIES = 'Non-GMO Cookies', OLIVE_OIL = 'Olive Oil', - REFRIGERATED_MEALS = 'Refrigerated Meals', - RICE_NOODLES = 'Rice Noodles', - SEED_BUTTERS = 'Seed Butters (Peanut Butter Alternative)', - WHOLE_GRAIN_COOKIES = 'Whole-Grain Cookies', QUINOA = 'Quinoa', + RICE_CERTIFIED_GLUTEN_FREE = 'Rice (Certified Gluten Free)', + SPREADS_SEED_BUTTERS = 'Spreads/Seed Butters (Peanut Butter Alternative)', + SNACKS = 'Snacks', + TEFF_FLOUR = 'Teff Flour', } diff --git a/apps/backend/src/foodRequests/dtos/create-request.dto.ts b/apps/backend/src/foodRequests/dtos/create-request.dto.ts index bc1d461fa..4eec589af 100644 --- a/apps/backend/src/foodRequests/dtos/create-request.dto.ts +++ b/apps/backend/src/foodRequests/dtos/create-request.dto.ts @@ -20,8 +20,17 @@ export class CreateRequestDto { @IsEnum(FoodType, { each: true }) requestedFoodTypes!: FoodType[]; + @IsString() + @IsNotEmpty() + location!: string; + @IsOptional() @IsString() @IsNotEmpty() additionalInformation?: string; + + @IsOptional() + @IsString() + @IsNotEmpty() + feedbackOnPriorDonation?: string; } diff --git a/apps/backend/src/foodRequests/dtos/food-request-summary.dto.ts b/apps/backend/src/foodRequests/dtos/food-request-summary.dto.ts index 2280f81f9..7a2c513e8 100644 --- a/apps/backend/src/foodRequests/dtos/food-request-summary.dto.ts +++ b/apps/backend/src/foodRequests/dtos/food-request-summary.dto.ts @@ -10,7 +10,9 @@ export class FoodRequestSummaryDto { requestId!: number; requestedSize!: RequestSize; requestedFoodTypes!: FoodType[]; + location!: string; additionalInformation!: string | null; + feedbackOnPriorDonation!: string | null; requestedAt!: Date; status!: FoodRequestStatus; pantry!: FoodRequestPantry; diff --git a/apps/backend/src/foodRequests/dtos/update-request.dto.ts b/apps/backend/src/foodRequests/dtos/update-request.dto.ts index 06c1fd277..dbf40a45f 100644 --- a/apps/backend/src/foodRequests/dtos/update-request.dto.ts +++ b/apps/backend/src/foodRequests/dtos/update-request.dto.ts @@ -18,8 +18,18 @@ export class UpdateRequestDto { @IsEnum(FoodType, { each: true }) requestedFoodTypes?: FoodType[]; + @IsOptional() + @IsString() + @IsNotEmpty() + location?: string; + @IsOptional() @IsString() @IsNotEmpty() additionalInformation?: string; + + @IsOptional() + @IsString() + @IsNotEmpty() + feedbackOnPriorDonation?: string; } diff --git a/apps/backend/src/foodRequests/request.controller.spec.ts b/apps/backend/src/foodRequests/request.controller.spec.ts index e5b7fa2cf..d6daba86d 100644 --- a/apps/backend/src/foodRequests/request.controller.spec.ts +++ b/apps/backend/src/foodRequests/request.controller.spec.ts @@ -150,7 +150,9 @@ describe('RequestsController', () => { FoodType.DAIRY_FREE_ALTERNATIVES, FoodType.DRIED_BEANS, ], + location: 'Boston, MA', additionalInformation: 'Test information.', + feedbackOnPriorDonation: 'Prior donation feedback.', }; const createdRequest: Partial = { @@ -173,7 +175,9 @@ describe('RequestsController', () => { createBody.pantryId, createBody.requestedSize, createBody.requestedFoodTypes, + createBody.location, createBody.additionalInformation, + createBody.feedbackOnPriorDonation, ); }); }); diff --git a/apps/backend/src/foodRequests/request.controller.ts b/apps/backend/src/foodRequests/request.controller.ts index 58a6592bd..eea51d455 100644 --- a/apps/backend/src/foodRequests/request.controller.ts +++ b/apps/backend/src/foodRequests/request.controller.ts @@ -133,11 +133,20 @@ export class RequestsController { items: { type: 'string', enum: Object.values(FoodType) }, example: [FoodType.DAIRY_FREE_ALTERNATIVES, FoodType.DRIED_BEANS], }, + location: { + type: 'string', + example: 'Boston, MA', + }, additionalInformation: { type: 'string', nullable: true, example: 'Urgent request', }, + feedbackOnPriorDonation: { + type: 'string', + nullable: true, + example: 'The last donation was well received by our clients.', + }, }, }, }) @@ -149,7 +158,9 @@ export class RequestsController { requestData.pantryId, requestData.requestedSize, requestData.requestedFoodTypes, + requestData.location, requestData.additionalInformation, + requestData.feedbackOnPriorDonation, ); } diff --git a/apps/backend/src/foodRequests/request.entity.ts b/apps/backend/src/foodRequests/request.entity.ts index c9a37b661..63bf74f52 100644 --- a/apps/backend/src/foodRequests/request.entity.ts +++ b/apps/backend/src/foodRequests/request.entity.ts @@ -56,6 +56,12 @@ export class FoodRequest { @Column({ name: 'additional_information', type: 'text', nullable: true }) additionalInformation!: string | null; + @Column({ name: 'location', type: 'text' }) + location!: string; + + @Column({ name: 'feedback_on_prior_donation', type: 'text', nullable: true }) + feedbackOnPriorDonation!: string | null; + @CreateDateColumn({ name: 'requested_at', type: 'timestamp', diff --git a/apps/backend/src/foodRequests/request.service.spec.ts b/apps/backend/src/foodRequests/request.service.spec.ts index 55ebda987..afba14e6c 100644 --- a/apps/backend/src/foodRequests/request.service.spec.ts +++ b/apps/backend/src/foodRequests/request.service.spec.ts @@ -137,7 +137,7 @@ describe('RequestsService', () => { const firstRequest = sorted[0]; expect(firstRequest.requestedSize).toBe(RequestSize.LARGE); expect(firstRequest.requestedFoodTypes).toEqual([ - FoodType.SEED_BUTTERS, + FoodType.SPREADS_SEED_BUTTERS, FoodType.GLUTEN_FREE_BREAD, FoodType.DRIED_BEANS, FoodType.DAIRY_FREE_ALTERNATIVES, @@ -174,13 +174,13 @@ describe('RequestsService', () => { id: 1, name: 'Peanut Butter (16oz)', quantity: 10, - foodType: 'Seed Butters (Peanut Butter Alternative)', + foodType: 'Spreads/Seed Butters (Peanut Butter Alternative)', }, { id: 3, name: 'Canned Green Beans', quantity: 5, - foodType: 'Refrigerated Meals', + foodType: 'Frozen Meals', }, { id: 2, @@ -211,11 +211,12 @@ describe('RequestsService', () => { it('should return empty list if no associated orders', async () => { const result = await testDataSource.query(` - INSERT INTO food_requests (pantry_id, requested_size, requested_food_types, requested_at) + INSERT INTO food_requests (pantry_id, requested_size, requested_food_types, location, requested_at) VALUES ( (SELECT pantry_id FROM pantries LIMIT 1), 'Small (2-5 boxes)', ARRAY[]::food_type_enum[], + 'Boston, MA', NOW() ) RETURNING request_id @@ -232,33 +233,40 @@ describe('RequestsService', () => { const result = await service.create( pantryId, RequestSize.MEDIUM, - [FoodType.DRIED_BEANS, FoodType.REFRIGERATED_MEALS], + [FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS], + 'Boston, MA', 'Additional info', + 'Prior donation was great', ); expect(result).toBeDefined(); expect(result.pantryId).toBe(pantryId); expect(result.requestedSize).toBe(RequestSize.MEDIUM); expect(result.requestedFoodTypes).toEqual([ FoodType.DRIED_BEANS, - FoodType.REFRIGERATED_MEALS, + FoodType.FROZEN_MEALS, ]); + expect(result.location).toBe('Boston, MA'); expect(result.additionalInformation).toBe('Additional info'); + expect(result.feedbackOnPriorDonation).toBe('Prior donation was great'); }); it('should successfully create and return new food request w/o additional info', async () => { const pantryId = 1; - const result = await service.create(pantryId, RequestSize.LARGE, [ - FoodType.GRANOLA, - FoodType.NUT_FREE_GRANOLA_BARS, - ]); + const result = await service.create( + pantryId, + RequestSize.LARGE, + [FoodType.GRANOLA, FoodType.GRANOLA_BARS], + 'Boston, MA', + ); expect(result).toBeDefined(); expect(result.pantryId).toBe(pantryId); expect(result.requestedSize).toBe(RequestSize.LARGE); expect(result.requestedFoodTypes).toEqual([ FoodType.GRANOLA, - FoodType.NUT_FREE_GRANOLA_BARS, + FoodType.GRANOLA_BARS, ]); expect(result.additionalInformation).toBeNull(); + expect(result.feedbackOnPriorDonation).toBeNull(); }); it('should send food request email to pantry user with volunteers BCCed', async () => { @@ -268,10 +276,12 @@ describe('RequestsService', () => { relations: ['pantryUser', 'volunteers'], }); - await service.create(pantryId, RequestSize.MEDIUM, [ - FoodType.DRIED_BEANS, - FoodType.REFRIGERATED_MEALS, - ]); + await service.create( + pantryId, + RequestSize.MEDIUM, + [FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS], + 'Boston, MA', + ); if (!pantry) throw new Error('Missing pantry test object'); const message = emailTemplates.pantrySubmitsFoodRequest({ @@ -300,10 +310,12 @@ describe('RequestsService', () => { relations: ['pantryUser', 'volunteers'], }); - await service.create(pantryId, RequestSize.MEDIUM, [ - FoodType.DRIED_BEANS, - FoodType.REFRIGERATED_MEALS, - ]); + await service.create( + pantryId, + RequestSize.MEDIUM, + [FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS], + 'Boston, MA', + ); if (!pantry) throw new Error('Missing pantry test object'); const volunteerEmails = (pantry.volunteers ?? []).map((v) => v.email); @@ -319,7 +331,12 @@ describe('RequestsService', () => { const pantryId = 1; await expect( - service.create(pantryId, RequestSize.MEDIUM, [FoodType.DRIED_BEANS]), + service.create( + pantryId, + RequestSize.MEDIUM, + [FoodType.DRIED_BEANS], + 'Boston, MA', + ), ).rejects.toThrow( new InternalServerErrorException( 'Failed to send new food request notification email to volunteers', @@ -335,7 +352,8 @@ describe('RequestsService', () => { service.create( 999, RequestSize.MEDIUM, - [FoodType.DRIED_BEANS, FoodType.REFRIGERATED_MEALS], + [FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS], + 'Boston, MA', 'Additional info', ), ).rejects.toThrow(new NotFoundException('Pantry 999 not found')); @@ -346,7 +364,8 @@ describe('RequestsService', () => { service.create( 4, RequestSize.MEDIUM, - [FoodType.DRIED_BEANS, FoodType.REFRIGERATED_MEALS], + [FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS], + 'Boston, MA', 'Additional info', ), ).rejects.toThrow(new ConflictException('Pantry 4 not approved')); @@ -357,7 +376,8 @@ describe('RequestsService', () => { service.create( 5, RequestSize.MEDIUM, - [FoodType.DRIED_BEANS, FoodType.REFRIGERATED_MEALS], + [FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS], + 'Boston, MA', 'Additional info', ), ).rejects.toThrow(new ConflictException('Pantry 5 not approved')); @@ -405,10 +425,12 @@ describe('RequestsService', () => { it('should throw BadRequestException for request with no orders', async () => { const pantryId = 1; - const result = await service.create(pantryId, RequestSize.MEDIUM, [ - FoodType.DRIED_BEANS, - FoodType.REFRIGERATED_MEALS, - ]); + const result = await service.create( + pantryId, + RequestSize.MEDIUM, + [FoodType.DRIED_BEANS, FoodType.FROZEN_MEALS], + 'Boston, MA', + ); const requestId = result.requestId; await expect(service.updateRequestStatus(requestId)).rejects.toThrow( @@ -741,7 +763,7 @@ describe('RequestsService', () => { FROM donations d WHERE di.donation_id = d.donation_id AND d.food_manufacturer_id = 2 - AND di.food_type IN ('Whole-Grain Cookies', 'Dairy-Free Alternatives', 'Nut-Free Granola Bars') + AND di.food_type IN ('Non-GMO Cookies', 'Dairy-Free Alternatives', 'Granola Bars') `); const result = await service.getAvailableItems(4, 2); expect(result.matchingItems).toHaveLength(0); @@ -785,7 +807,7 @@ describe('RequestsService', () => { const fromDb = await service.findOne(1); expect(fromDb.requestedSize).toBe(RequestSize.MEDIUM); expect(fromDb.requestedFoodTypes).toEqual([ - FoodType.SEED_BUTTERS, + FoodType.SPREADS_SEED_BUTTERS, FoodType.GLUTEN_FREE_BREAD, FoodType.DRIED_BEANS, FoodType.DAIRY_FREE_ALTERNATIVES, @@ -807,13 +829,17 @@ describe('RequestsService', () => { await service.update(1, { requestedSize: RequestSize.SMALL, requestedFoodTypes: [FoodType.GRANOLA], + location: 'Cambridge, MA', additionalInformation: 'Updated information', + feedbackOnPriorDonation: 'Updated feedback', }); const fromDb = await service.findOne(1); expect(fromDb.requestedSize).toBe(RequestSize.SMALL); expect(fromDb.requestedFoodTypes).toEqual([FoodType.GRANOLA]); + expect(fromDb.location).toBe('Cambridge, MA'); expect(fromDb.additionalInformation).toBe('Updated information'); + expect(fromDb.feedbackOnPriorDonation).toBe('Updated feedback'); }); it('should throw BadRequestException when request is not active', async () => { diff --git a/apps/backend/src/foodRequests/request.service.ts b/apps/backend/src/foodRequests/request.service.ts index 2aa9d8c83..f9c77a31f 100644 --- a/apps/backend/src/foodRequests/request.service.ts +++ b/apps/backend/src/foodRequests/request.service.ts @@ -64,7 +64,9 @@ export class RequestsService { 'request.requestId', 'request.requestedSize', 'request.requestedFoodTypes', + 'request.location', 'request.additionalInformation', + 'request.feedbackOnPriorDonation', 'request.requestedAt', 'request.status', 'pantry.pantryId', @@ -234,7 +236,9 @@ export class RequestsService { pantryId: number, requestedSize: RequestSize, requestedFoodTypes: FoodType[], + location: string, additionalInformation?: string, + feedbackOnPriorDonation?: string, ): Promise { validateId(pantryId, 'Pantry'); @@ -255,7 +259,9 @@ export class RequestsService { pantryId, requestedSize, requestedFoodTypes, + location, additionalInformation, + feedbackOnPriorDonation, }); await this.repo.save(foodRequest); @@ -373,7 +379,9 @@ export class RequestsService { if ( dto.requestedSize == undefined && dto.requestedFoodTypes == undefined && - dto.additionalInformation == undefined + dto.location == undefined && + dto.additionalInformation == undefined && + dto.feedbackOnPriorDonation == undefined ) { throw new BadRequestException( 'At least one field must be provided to update request', diff --git a/apps/backend/src/migrations/1781476891610-UpdateFoodTypesAndOrder.ts b/apps/backend/src/migrations/1781476891610-UpdateFoodTypesAndOrder.ts new file mode 100644 index 000000000..1021ca0b8 --- /dev/null +++ b/apps/backend/src/migrations/1781476891610-UpdateFoodTypesAndOrder.ts @@ -0,0 +1,179 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateFoodRequestTypesAndOrder1781476891610 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE food_requests + ADD COLUMN location text NOT NULL DEFAULT '', + ADD COLUMN feedback_on_prior_donation text; + `); + + await queryRunner.query(` + ALTER TYPE "food_type_enum" RENAME TO "food_type_enum_old"; + `); + + await queryRunner.query(` + CREATE TYPE "food_type_enum" AS ENUM ( + 'Dairy-Free Alternatives', + 'Dried Beans', + 'Frozen Meals', + 'Gluten-Free Baking/Pancake Mixes', + 'Gluten-Free Bread', + 'Gluten-Free Pasta', + 'Gluten-Free Tortillas (Frozen)', + 'Granola', + 'Granola Bars', + 'Masa Harina Flour', + 'Non-GMO Cookies', + 'Olive Oil', + 'Quinoa', + 'Rice (Certified Gluten Free)', + 'Spreads/Seed Butters (Peanut Butter Alternative)', + 'Snacks', + 'Teff Flour' + ); + `); + + await queryRunner.query(` + ALTER TABLE donation_items + ALTER COLUMN food_type TYPE "food_type_enum" + USING ( + CASE food_type::text + WHEN 'Dried Beans (Gluten-Free, Nut-Free)' THEN 'Dried Beans' + WHEN 'Gluten-Free Tortillas' THEN 'Gluten-Free Tortillas (Frozen)' + WHEN 'Nut-Free Granola Bars' THEN 'Granola Bars' + WHEN 'Seed Butters (Peanut Butter Alternative)' THEN 'Spreads/Seed Butters (Peanut Butter Alternative)' + WHEN 'Refrigerated Meals' THEN 'Frozen Meals' + WHEN 'Whole-Grain Cookies' THEN 'Non-GMO Cookies' + WHEN 'Rice Noodles' THEN 'Gluten-Free Pasta' + ELSE food_type::text + END + )::"food_type_enum"; + `); + + await queryRunner.query(` + ALTER TABLE food_requests + ALTER COLUMN requested_food_types TYPE text[] + USING requested_food_types::text[]; + `); + + await queryRunner.query(` + UPDATE food_requests + SET requested_food_types = + array_replace( + array_replace( + array_replace( + array_replace( + array_replace( + array_replace( + array_replace(requested_food_types, + 'Dried Beans (Gluten-Free, Nut-Free)', 'Dried Beans'), + 'Gluten-Free Tortillas', 'Gluten-Free Tortillas (Frozen)'), + 'Nut-Free Granola Bars', 'Granola Bars'), + 'Seed Butters (Peanut Butter Alternative)', 'Spreads/Seed Butters (Peanut Butter Alternative)'), + 'Refrigerated Meals', 'Frozen Meals'), + 'Whole-Grain Cookies', 'Non-GMO Cookies'), + 'Rice Noodles', 'Gluten-Free Pasta'); + `); + + await queryRunner.query(` + ALTER TABLE food_requests + ALTER COLUMN requested_food_types TYPE "food_type_enum"[] + USING requested_food_types::"food_type_enum"[]; + `); + + await queryRunner.query(`DROP TYPE "food_type_enum_old";`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TYPE "food_type_enum" RENAME TO "food_type_enum_new"; + `); + + await queryRunner.query(` + CREATE TYPE "food_type_enum" AS ENUM ( + 'Dairy-Free Alternatives', + 'Dried Beans (Gluten-Free, Nut-Free)', + 'Gluten-Free Baking/Pancake Mixes', + 'Gluten-Free Bread', + 'Gluten-Free Tortillas', + 'Granola', + 'Masa Harina Flour', + 'Nut-Free Granola Bars', + 'Olive Oil', + 'Refrigerated Meals', + 'Rice Noodles', + 'Seed Butters (Peanut Butter Alternative)', + 'Whole-Grain Cookies', + 'Quinoa' + ); + `); + + await queryRunner.query(` + ALTER TABLE donation_items + ALTER COLUMN food_type TYPE "food_type_enum" + USING ( + CASE food_type::text + WHEN 'Dried Beans' THEN 'Dried Beans (Gluten-Free, Nut-Free)' + WHEN 'Gluten-Free Tortillas (Frozen)' THEN 'Gluten-Free Tortillas' + WHEN 'Granola Bars' THEN 'Nut-Free Granola Bars' + WHEN 'Spreads/Seed Butters (Peanut Butter Alternative)' THEN 'Seed Butters (Peanut Butter Alternative)' + WHEN 'Frozen Meals' THEN 'Refrigerated Meals' + WHEN 'Non-GMO Cookies' THEN 'Whole-Grain Cookies' + WHEN 'Gluten-Free Pasta' THEN 'Rice Noodles' + WHEN 'Rice (Certified Gluten Free)' THEN 'Rice Noodles' + WHEN 'Snacks' THEN 'Whole-Grain Cookies' + WHEN 'Teff Flour' THEN 'Masa Harina Flour' + ELSE food_type::text + END + )::"food_type_enum"; + `); + + await queryRunner.query(` + ALTER TABLE food_requests + ALTER COLUMN requested_food_types TYPE text[] + USING requested_food_types::text[]; + `); + + await queryRunner.query(` + UPDATE food_requests + SET requested_food_types = + array_replace( + array_replace( + array_replace( + array_replace( + array_replace( + array_replace( + array_replace( + array_replace( + array_replace( + array_replace(requested_food_types, + 'Dried Beans', 'Dried Beans (Gluten-Free, Nut-Free)'), + 'Gluten-Free Tortillas (Frozen)', 'Gluten-Free Tortillas'), + 'Granola Bars', 'Nut-Free Granola Bars'), + 'Spreads/Seed Butters (Peanut Butter Alternative)', 'Seed Butters (Peanut Butter Alternative)'), + 'Frozen Meals', 'Refrigerated Meals'), + 'Non-GMO Cookies', 'Whole-Grain Cookies'), + 'Gluten-Free Pasta', 'Rice Noodles'), + 'Rice (Certified Gluten Free)', 'Rice Noodles'), + 'Snacks', 'Whole-Grain Cookies'), + 'Teff Flour', 'Masa Harina Flour'); + `); + + await queryRunner.query(` + ALTER TABLE food_requests + ALTER COLUMN requested_food_types TYPE "food_type_enum"[] + USING requested_food_types::"food_type_enum"[]; + `); + + await queryRunner.query(`DROP TYPE "food_type_enum_new";`); + + await queryRunner.query(` + ALTER TABLE food_requests + DROP COLUMN feedback_on_prior_donation, + DROP COLUMN location; + `); + } +} diff --git a/apps/backend/src/orders/order.module.ts b/apps/backend/src/orders/order.module.ts index 0e1e01246..486bfc29a 100644 --- a/apps/backend/src/orders/order.module.ts +++ b/apps/backend/src/orders/order.module.ts @@ -20,6 +20,7 @@ import { Donation } from '../donations/donations.entity'; import { EmailsModule } from '../emails/email.module'; import { User } from '../users/users.entity'; import { UsersModule } from '../users/users.module'; +import { PantriesModule } from '../pantries/pantries.module'; @Module({ imports: [ @@ -38,6 +39,7 @@ import { UsersModule } from '../users/users.module'; AWSS3Module, MulterModule.register({ dest: './uploads' }), forwardRef(() => RequestsModule), + forwardRef(() => PantriesModule), ManufacturerModule, DonationItemsModule, DonationModule, diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index 3d32f52a5..ce460eb54 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -261,7 +261,7 @@ describe('OrdersService', () => { items: [ { id: 1, - foodType: FoodType.SEED_BUTTERS, + foodType: FoodType.SPREADS_SEED_BUTTERS, name: 'Peanut Butter (16oz)', quantity: 10, }, @@ -273,7 +273,7 @@ describe('OrdersService', () => { }, { id: 3, - foodType: FoodType.REFRIGERATED_MEALS, + foodType: FoodType.FROZEN_MEALS, name: 'Canned Green Beans', quantity: 5, }, diff --git a/apps/backend/src/orders/order.service.ts b/apps/backend/src/orders/order.service.ts index 166c9c603..5fcc1b9b7 100644 --- a/apps/backend/src/orders/order.service.ts +++ b/apps/backend/src/orders/order.service.ts @@ -456,7 +456,9 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ requestId: true, requestedSize: true, requestedFoodTypes: true, + location: true, additionalInformation: true, + feedbackOnPriorDonation: true, requestedAt: true, status: true, pantry: { @@ -476,7 +478,9 @@ ${request.pantry.shipmentAddressCity}, ${request.pantry.shipmentAddressState} ${ requestId: order.request.requestId, requestedSize: order.request.requestedSize, requestedFoodTypes: order.request.requestedFoodTypes, + location: order.request.location, additionalInformation: order.request.additionalInformation ?? null, + feedbackOnPriorDonation: order.request.feedbackOnPriorDonation ?? null, requestedAt: order.request.requestedAt, status: order.request.status, pantry: { diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index 376b27c6d..a904eca69 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -83,7 +83,9 @@ export class VolunteersService { requestId: r.requestId, requestedSize: r.requestedSize, requestedFoodTypes: r.requestedFoodTypes, + location: r.location, additionalInformation: r.additionalInformation, + feedbackOnPriorDonation: r.feedbackOnPriorDonation, requestedAt: r.requestedAt, status: r.status, pantry: { diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx index 23c4e9e80..4e354153d 100644 --- a/apps/frontend/src/components/forms/requestDetailsModal.tsx +++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx @@ -43,7 +43,9 @@ const RequestDetailsModal: React.FC = ({ const requestedSize = request.requestedSize; const selectedFoodTypes = request.requestedFoodTypes; + const location = request.location; const additionalNotes = request.additionalInformation; + const feedbackOnPriorDonation = request.feedbackOnPriorDonation; useEffect(() => { const fetchRequestOrderDetails = async () => { @@ -159,6 +161,28 @@ const RequestDetailsModal: React.FC = ({ + + + + Location + + + + {location} + + + + + + + Feedback on Prior Donation + + + + {feedbackOnPriorDonation} + + + diff --git a/apps/frontend/src/components/forms/requestFormModal.tsx b/apps/frontend/src/components/forms/requestFormModal.tsx index 6754abcad..68b8ee831 100644 --- a/apps/frontend/src/components/forms/requestFormModal.tsx +++ b/apps/frontend/src/components/forms/requestFormModal.tsx @@ -3,6 +3,7 @@ import { Flex, Button, Textarea, + Input, Menu, Text, Dialog, @@ -42,21 +43,46 @@ const FoodRequestFormModal: React.FC = ({ useModalBodyCleanup(); const [selectedFoodTypes, setSelectedFoodTypes] = useState([]); const [requestedSize, setRequestedSize] = useState(''); + const [locationCity, setLocationCity] = useState(''); + const [locationState, setLocationState] = useState(''); const [additionalNotes, setAdditionalNotes] = useState(''); + const [feedbackOnPriorDonation, setFeedbackOnPriorDonation] = + useState(''); const [alertState, setAlertMessage] = useAlert(); - const isFormValid = requestedSize !== '' && selectedFoodTypes.length > 0; + const isFormValid = + requestedSize !== '' && + selectedFoodTypes.length > 0 && + locationCity.trim() !== '' && + locationState.trim() !== ''; useEffect(() => { if (isOpen) { if (previousRequest) { setSelectedFoodTypes(previousRequest.requestedFoodTypes || []); setRequestedSize(previousRequest.requestedSize || ''); + // location is stored as a single "City, State" string; split it back + // out on the last comma so each input is prefilled independently. + const prevLocation = previousRequest.location || ''; + const commaIdx = prevLocation.lastIndexOf(','); + if (commaIdx >= 0) { + setLocationCity(prevLocation.slice(0, commaIdx).trim()); + setLocationState(prevLocation.slice(commaIdx + 1).trim()); + } else { + setLocationCity(prevLocation.trim()); + setLocationState(''); + } setAdditionalNotes(previousRequest.additionalInformation || ''); + setFeedbackOnPriorDonation( + previousRequest.feedbackOnPriorDonation || '', + ); } else { setSelectedFoodTypes([]); setRequestedSize(''); + setLocationCity(''); + setLocationState(''); setAdditionalNotes(''); + setFeedbackOnPriorDonation(''); } } }, [isOpen, previousRequest, setAlertMessage]); @@ -65,7 +91,9 @@ const FoodRequestFormModal: React.FC = ({ const foodRequestData: CreateFoodRequestBody = { pantryId, requestedSize: requestedSize as RequestSize, + location: `${locationCity.trim()}, ${locationState.trim()}`, additionalInformation: additionalNotes || undefined, + feedbackOnPriorDonation: feedbackOnPriorDonation || undefined, requestedFoodTypes: selectedFoodTypes, }; @@ -189,9 +217,7 @@ const FoodRequestFormModal: React.FC = ({ justifyContent="space-between" textStyle="p2" > - {selectedFoodTypes.length > 0 - ? `Select more food types` - : 'Select food types'} + Select more food types @@ -250,6 +276,89 @@ const FoodRequestFormModal: React.FC = ({ /> + + + + Location + + + + setLocationCity(e.target.value)} + /> + setLocationState(e.target.value)} + /> + + + + + + + Feedback on Prior Donation + + +