diff --git a/apps/backend/src/config/migrations.ts b/apps/backend/src/config/migrations.ts index 65a88e352..b60dccae8 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 { UpdatePantryFMApplicationInfo1780913024514 } from '../migrations/1780913024514-UpdatePantryFMApplicationInfo'; const schemaMigrations = [ User1725726359198, @@ -86,6 +87,7 @@ const schemaMigrations = [ AddDonationItemConfirmation1774140453305, DonationItemsOnDeleteCascade1774214910101, OrdersVolunteerActions1774883880543, + UpdatePantryFMApplicationInfo1780913024514, ]; export default schemaMigrations; diff --git a/apps/backend/src/donations/donations.service.spec.ts b/apps/backend/src/donations/donations.service.spec.ts index 6fa0bcda3..0d2361b43 100644 --- a/apps/backend/src/donations/donations.service.spec.ts +++ b/apps/backend/src/donations/donations.service.spec.ts @@ -235,10 +235,12 @@ describe('DonationService', () => { expect(d.foodManufacturer).toBeDefined(); }); - const firstDonation = donations[0]; - expect(firstDonation.status).toBe(DonationStatus.MATCHED); - expect(firstDonation.foodManufacturer.foodManufacturerId).toBe(2); - expect(firstDonation.recurrence).toBe(RecurrenceEnum.NONE); + const matchedDonation = donations.find( + (d) => d.status === DonationStatus.MATCHED, + ); + expect(matchedDonation).toBeDefined(); + expect(matchedDonation?.foodManufacturer.foodManufacturerId).toBe(2); + expect(matchedDonation?.recurrence).toBe(RecurrenceEnum.NONE); }); }); diff --git a/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts b/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts index 556e6910a..e5cacdad1 100644 --- a/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts +++ b/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts @@ -10,7 +10,7 @@ import { Length, MaxLength, } from 'class-validator'; -import { Allergen, DonateWastedFood, ManufacturerAttribute } from '../types'; +import { Allergen, DonateWastedFood } from '../types'; export class FoodManufacturerApplicationDto { @IsString() @@ -84,9 +84,6 @@ export class FoodManufacturerApplicationDto { @IsBoolean() productsGlutenFree!: boolean; - @IsBoolean() - productsContainSulfites!: boolean; - @IsString() @IsNotEmpty() productsSustainableExplanation!: string; @@ -97,16 +94,8 @@ export class FoodManufacturerApplicationDto { @IsEnum(DonateWastedFood) donateWastedFood!: DonateWastedFood; - @IsOptional() - @IsEnum(ManufacturerAttribute) - manufacturerAttribute?: ManufacturerAttribute; - @IsOptional() @IsString() @IsNotEmpty() additionalComments?: string; - - @IsOptional() - @IsBoolean() - newsletterSubscription?: boolean; } diff --git a/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts b/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts index a1078bdd0..7130c690e 100644 --- a/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts +++ b/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts @@ -10,7 +10,7 @@ import { Length, MaxLength, } from 'class-validator'; -import { Allergen, DonateWastedFood, ManufacturerAttribute } from '../types'; +import { Allergen, DonateWastedFood } from '../types'; export class UpdateFoodManufacturerApplicationDto { @IsOptional() @@ -69,10 +69,6 @@ export class UpdateFoodManufacturerApplicationDto { @IsBoolean() productsGlutenFree?: boolean; - @IsOptional() - @IsBoolean() - productsContainSulfites?: boolean; - @IsOptional() @IsString() @IsNotEmpty() @@ -86,16 +82,8 @@ export class UpdateFoodManufacturerApplicationDto { @IsEnum(DonateWastedFood) donateWastedFood?: DonateWastedFood; - @IsOptional() - @IsEnum(ManufacturerAttribute) - manufacturerAttribute?: ManufacturerAttribute; - @IsOptional() @IsString() @IsNotEmpty() additionalComments?: string; - - @IsOptional() - @IsBoolean() - newsletterSubscription?: boolean; } diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts index f59dd7c77..542e94681 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.controller.spec.ts @@ -196,7 +196,6 @@ describe('FoodManufacturersController', () => { unlistedProductAllergens: [Allergen.EGG], facilityFreeAllergens: [Allergen.EGG], productsGlutenFree: true, - productsContainSulfites: false, productsSustainableExplanation: 'We use eco-friendly packaging.', inKindDonations: true, donateWastedFood: DonateWastedFood.SOMETIMES, diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.ts index b56ad4a86..b07be755c 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.controller.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.controller.ts @@ -13,7 +13,7 @@ import { FoodManufacturersService } from './manufacturers.service'; import { FoodManufacturer } from './manufacturers.entity'; import { FoodManufacturerApplicationDto } from './dtos/manufacturer-application.dto'; import { ApiBody } from '@nestjs/swagger'; -import { Allergen, DonateWastedFood, ManufacturerAttribute } from './types'; +import { Allergen, DonateWastedFood } from './types'; import { Public } from '../auth/public.decorator'; import { UpdateFoodManufacturerApplicationDto } from './dtos/update-manufacturer-application.dto'; import { Roles } from '../auth/roles.decorator'; @@ -155,10 +155,6 @@ export class FoodManufacturersController { type: 'boolean', example: true, }, - productsContainSulfites: { - type: 'boolean', - example: false, - }, productsSustainableExplanation: { type: 'string', example: 'Our products are environmentally conscious.', @@ -172,19 +168,10 @@ export class FoodManufacturersController { enum: Object.values(DonateWastedFood), example: DonateWastedFood.ALWAYS, }, - manufacturerAttribute: { - type: 'string', - enum: Object.values(ManufacturerAttribute), - example: ManufacturerAttribute.ORGANIC, - }, additionalComments: { type: 'string', example: 'Nope!', }, - newsletterSubscription: { - type: 'boolean', - example: true, - }, }, required: [ 'foodManufacturerName', @@ -196,7 +183,6 @@ export class FoodManufacturersController { 'unlistedProductAllergens', 'facilityFreeAllergens', 'productsGlutenFree', - 'productsContainSulfites', 'productsSustainableExplanation', 'inKindDonations', 'donateWastedFood', diff --git a/apps/backend/src/foodManufacturers/manufacturers.entity.ts b/apps/backend/src/foodManufacturers/manufacturers.entity.ts index 3cff3e051..0ab81abd6 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.entity.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.entity.ts @@ -8,7 +8,7 @@ import { } from 'typeorm'; import { User } from '../users/users.entity'; import { Donation } from '../donations/donations.entity'; -import { Allergen, DonateWastedFood, ManufacturerAttribute } from './types'; +import { Allergen, DonateWastedFood } from './types'; import { ApplicationStatus } from '../shared/types'; @Entity('food_manufacturers') @@ -86,9 +86,6 @@ export class FoodManufacturer { @Column({ name: 'products_gluten_free', type: 'boolean' }) productsGlutenFree!: boolean; - @Column({ name: 'products_contain_sulfites', type: 'boolean' }) - productsContainSulfites!: boolean; - @Column({ name: 'products_sustainable_explanation', type: 'text', @@ -106,15 +103,6 @@ export class FoodManufacturer { }) donateWastedFood!: DonateWastedFood; - @Column({ - name: 'manufacturer_attribute', - type: 'enum', - enum: ManufacturerAttribute, - enumName: 'manufacturer_attribute_enum', - nullable: true, - }) - manufacturerAttribute!: ManufacturerAttribute | null; - @Column({ name: 'additional_comments', type: 'text', @@ -122,9 +110,6 @@ export class FoodManufacturer { }) additionalComments!: string | null; - @Column({ name: 'newsletter_subscription', type: 'boolean', nullable: true }) - newsletterSubscription!: boolean | null; - @OneToMany(() => Donation, (donation) => donation.foodManufacturer) donations!: Donation[]; diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts index 5a2f5e7ad..d0cbf6981 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.spec.ts @@ -20,7 +20,7 @@ import { AuthService } from '../auth/auth.service'; import { EmailsService } from '../emails/email.service'; import { mock } from 'jest-mock-extended'; import { emailTemplates, SSF_PARTNER_EMAIL } from '../emails/emailTemplates'; -import { Allergen, DonateWastedFood, ManufacturerAttribute } from './types'; +import { Allergen, DonateWastedFood } from './types'; import { DonationItemsService } from '../donationItems/donationItems.service'; import { DonationItem } from '../donationItems/donationItems.entity'; import { DataSource } from 'typeorm'; @@ -48,7 +48,6 @@ const dto: FoodManufacturerApplicationDto = { unlistedProductAllergens: [Allergen.SHELLFISH, Allergen.TREE_NUTS], facilityFreeAllergens: [Allergen.PEANUT, Allergen.FISH], productsGlutenFree: false, - productsContainSulfites: false, productsSustainableExplanation: 'none', inKindDonations: false, donateWastedFood: DonateWastedFood.ALWAYS, @@ -296,9 +295,7 @@ describe('FoodManufacturersService', () => { secondaryContactLastName: 'Johnson', secondaryContactEmail: 'sarah.johnson@example.com', secondaryContactPhone: '555-555-5557', - manufacturerAttribute: ManufacturerAttribute.ORGANIC, additionalComments: 'We specialize in allergen-free products', - newsletterSubscription: true, }; await service.addFoodManufacturer(optionalDto); @@ -314,7 +311,6 @@ describe('FoodManufacturersService', () => { ); expect(saved?.status).toBe(ApplicationStatus.PENDING); expect(saved?.secondaryContactFirstName).toBe('Sarah'); - expect(saved?.manufacturerAttribute).toBe(ManufacturerAttribute.ORGANIC); }); it('should still save manufacturer to database if representative email send fails', async () => { diff --git a/apps/backend/src/foodManufacturers/manufacturers.service.ts b/apps/backend/src/foodManufacturers/manufacturers.service.ts index d2b4b4d84..0c52fd131 100644 --- a/apps/backend/src/foodManufacturers/manufacturers.service.ts +++ b/apps/backend/src/foodManufacturers/manufacturers.service.ts @@ -270,18 +270,12 @@ export class FoodManufacturersService { foodManufacturerData.facilityFreeAllergens; foodManufacturer.productsGlutenFree = foodManufacturerData.productsGlutenFree; - foodManufacturer.productsContainSulfites = - foodManufacturerData.productsContainSulfites; foodManufacturer.productsSustainableExplanation = foodManufacturerData.productsSustainableExplanation; foodManufacturer.inKindDonations = foodManufacturerData.inKindDonations; foodManufacturer.donateWastedFood = foodManufacturerData.donateWastedFood; - foodManufacturer.manufacturerAttribute = - foodManufacturerData.manufacturerAttribute ?? null; foodManufacturer.additionalComments = foodManufacturerData.additionalComments ?? null; - foodManufacturer.newsletterSubscription = - foodManufacturerData.newsletterSubscription ?? null; await this.repo.save(foodManufacturer); diff --git a/apps/backend/src/foodManufacturers/types.ts b/apps/backend/src/foodManufacturers/types.ts index 96f7ac584..66b469b33 100644 --- a/apps/backend/src/foodManufacturers/types.ts +++ b/apps/backend/src/foodManufacturers/types.ts @@ -22,12 +22,4 @@ export enum Allergen { FISH = 'Fish', SHELLFISH = 'Shellfish', SESAME = 'Sesame', - GLUTEN = 'Gluten', -} - -export enum ManufacturerAttribute { - FEMALE = 'Female-founded or women-led', - NON_GMO = 'Non-GMO Project Verified', - ORGANIC = 'USDA Certified Organic', - NONE = 'None of the above', } diff --git a/apps/backend/src/migrations/1780913024514-UpdatePantryFMApplicationInfo.ts b/apps/backend/src/migrations/1780913024514-UpdatePantryFMApplicationInfo.ts new file mode 100644 index 000000000..4e4ebc825 --- /dev/null +++ b/apps/backend/src/migrations/1780913024514-UpdatePantryFMApplicationInfo.ts @@ -0,0 +1,218 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdatePantryFMApplicationInfo1780913024514 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TYPE "dedicated_allergy_friendly_enum" AS ENUM ( + 'Yes', + 'No but we can accommodate this', + 'No, and we cannot accommodate this' + ); + `); + + await queryRunner.query(` + UPDATE pantries + SET + delivery_window_instructions = COALESCE(delivery_window_instructions, 'N/A'), + client_visit_frequency = COALESCE(client_visit_frequency, 'Daily'), + serve_allergic_children = COALESCE(serve_allergic_children, 'Yes, many (> 10)'), + shipment_address_country = COALESCE(shipment_address_country, 'US'), + mailing_address_country = COALESCE(mailing_address_country, 'US'), + activities = array_replace(activities, 'Spreadsheet to track dietary needs', 'Create labeled shelf') + WHERE delivery_window_instructions IS NULL + OR client_visit_frequency IS NULL + OR serve_allergic_children IS NULL + OR shipment_address_country IS NULL + OR mailing_address_country IS NULL + OR 'Spreadsheet to track dietary needs' = ANY(activities); + `); + + await queryRunner.query(` + ALTER TABLE pantries + ADD COLUMN languages text[] NOT NULL DEFAULT '{English}', + ALTER COLUMN dedicated_allergy_friendly TYPE dedicated_allergy_friendly_enum + USING ( + CASE + WHEN dedicated_allergy_friendly THEN 'Yes' + ELSE 'No, and we cannot accommodate this' + END + )::dedicated_allergy_friendly_enum, + ALTER COLUMN delivery_window_instructions SET NOT NULL, + ALTER COLUMN client_visit_frequency SET NOT NULL, + ALTER COLUMN serve_allergic_children SET NOT NULL, + ALTER COLUMN shipment_address_country SET DEFAULT 'US', + ALTER COLUMN shipment_address_country SET NOT NULL, + ALTER COLUMN mailing_address_country SET DEFAULT 'US', + ALTER COLUMN mailing_address_country SET NOT NULL, + DROP COLUMN identify_allergens_confidence, + DROP COLUMN newsletter_subscription; + `); + + await queryRunner.query(` + ALTER TABLE pantries + ALTER COLUMN languages DROP DEFAULT; + `); + + await queryRunner.query(`DROP TYPE "allergens_confidence_enum";`); + + await queryRunner.query(` + ALTER TYPE "activity_enum" RENAME TO "activity_enum_old"; + + CREATE TYPE "activity_enum" AS ENUM ( + 'Create labeled shelf', + 'Provide educational pamphlets', + 'Post allergen-free resource flyers', + 'Survey clients to determine medical dietary needs', + 'Collect feedback from allergen-avoidant clients', + 'Something else' + ); + + ALTER TABLE pantries + ALTER COLUMN activities TYPE "activity_enum"[] + USING activities::text[]::"activity_enum"[], + ALTER COLUMN activities DROP NOT NULL; + + DROP TYPE "activity_enum_old"; + `); + + await queryRunner.query(` + ALTER TABLE food_manufacturers + DROP COLUMN products_contain_sulfites, + DROP COLUMN manufacturer_attribute, + DROP COLUMN newsletter_subscription; + `); + + await queryRunner.query(`DROP TYPE "manufacturer_attribute_enum";`); + + await queryRunner.query(` + ALTER TABLE food_manufacturers + ALTER COLUMN unlisted_product_allergens DROP DEFAULT, + ALTER COLUMN facility_free_allergens DROP DEFAULT; + + UPDATE food_manufacturers + SET + unlisted_product_allergens = array_remove(unlisted_product_allergens, 'Gluten'), + facility_free_allergens = array_remove(facility_free_allergens, 'Gluten') + WHERE 'Gluten' = ANY(unlisted_product_allergens) + OR 'Gluten' = ANY(facility_free_allergens); + + ALTER TYPE "allergen_enum" RENAME TO "allergen_enum_old"; + + CREATE TYPE "allergen_enum" AS ENUM ( + 'Milk', + 'Egg', + 'Peanut', + 'Tree nuts', + 'Wheat', + 'Soy', + 'Fish', + 'Shellfish', + 'Sesame' + ); + + ALTER TABLE food_manufacturers + ALTER COLUMN unlisted_product_allergens TYPE "allergen_enum"[] + USING unlisted_product_allergens::text[]::"allergen_enum"[], + ALTER COLUMN facility_free_allergens TYPE "allergen_enum"[] + USING facility_free_allergens::text[]::"allergen_enum"[]; + + DROP TYPE "allergen_enum_old"; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TYPE "allergen_enum" RENAME TO "allergen_enum_old"; + + CREATE TYPE "allergen_enum" AS ENUM ( + 'Milk', + 'Egg', + 'Peanut', + 'Tree nuts', + 'Wheat', + 'Soy', + 'Fish', + 'Shellfish', + 'Sesame', + 'Gluten' + ); + + ALTER TABLE food_manufacturers + ALTER COLUMN unlisted_product_allergens TYPE "allergen_enum"[] + USING unlisted_product_allergens::text[]::"allergen_enum"[], + ALTER COLUMN facility_free_allergens TYPE "allergen_enum"[] + USING facility_free_allergens::text[]::"allergen_enum"[]; + + DROP TYPE "allergen_enum_old"; + `); + + await queryRunner.query(` + CREATE TYPE "manufacturer_attribute_enum" AS ENUM ( + 'Female-founded or women-led', + 'Non-GMO Project Verified', + 'USDA Certified Organic', + 'None of the above' + ); + + ALTER TABLE food_manufacturers + ADD COLUMN products_contain_sulfites BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN manufacturer_attribute manufacturer_attribute_enum, + ADD COLUMN newsletter_subscription BOOLEAN; + `); + + await queryRunner.query(` + ALTER TYPE "activity_enum" RENAME TO "activity_enum_old"; + + CREATE TYPE "activity_enum" AS ENUM ( + 'Create labeled shelf', + 'Provide educational pamphlets', + 'Spreadsheet to track dietary needs', + 'Post allergen-free resource flyers', + 'Survey clients to determine medical dietary needs', + 'Collect feedback from allergen-avoidant clients', + 'Something else' + ); + + ALTER TABLE pantries + ALTER COLUMN activities TYPE "activity_enum"[] + USING activities::text[]::"activity_enum"[]; + + UPDATE pantries + SET activities = ARRAY['Create labeled shelf']::"activity_enum"[] + WHERE activities IS NULL; + + ALTER TABLE pantries + ALTER COLUMN activities SET NOT NULL; + + DROP TYPE "activity_enum_old"; + `); + + await queryRunner.query(` + CREATE TYPE "allergens_confidence_enum" AS ENUM ( + 'Very confident', + 'Somewhat confident', + 'Not very confident (we need more education!)' + ); + `); + + await queryRunner.query(` + ALTER TABLE pantries + ADD COLUMN identify_allergens_confidence allergens_confidence_enum, + ADD COLUMN newsletter_subscription boolean, + ALTER COLUMN delivery_window_instructions DROP NOT NULL, + ALTER COLUMN client_visit_frequency DROP NOT NULL, + ALTER COLUMN serve_allergic_children DROP NOT NULL, + ALTER COLUMN shipment_address_country DROP NOT NULL, + ALTER COLUMN shipment_address_country DROP DEFAULT, + ALTER COLUMN mailing_address_country DROP NOT NULL, + ALTER COLUMN mailing_address_country DROP DEFAULT, + ALTER COLUMN dedicated_allergy_friendly TYPE boolean + USING (dedicated_allergy_friendly = 'Yes'), + DROP COLUMN languages; + `); + + await queryRunner.query(`DROP TYPE "dedicated_allergy_friendly_enum";`); + } +} diff --git a/apps/backend/src/pantries/dtos/pantry-application.dto.ts b/apps/backend/src/pantries/dtos/pantry-application.dto.ts index 3a71c1762..d908dcf51 100644 --- a/apps/backend/src/pantries/dtos/pantry-application.dto.ts +++ b/apps/backend/src/pantries/dtos/pantry-application.dto.ts @@ -14,7 +14,7 @@ import { RefrigeratedDonation, ReserveFoodForAllergic, ClientVisitFrequency, - AllergensConfidence, + DedicatedAllergyFriendly, ServeAllergicChildren, Activity, } from '../types'; @@ -111,11 +111,10 @@ export class PantryApplicationDto { @Length(1, 255) shipmentAddressZip!: string; - @IsOptional() @IsString() - @MaxLength(255) @IsNotEmpty() - shipmentAddressCountry?: string; + @Length(1, 255) + shipmentAddressCountry!: string; @IsString() @IsNotEmpty() @@ -143,11 +142,10 @@ export class PantryApplicationDto { @Length(1, 255) mailingAddressZip!: string; - @IsOptional() @IsString() - @MaxLength(255) @IsNotEmpty() - mailingAddressCountry?: string; + @Length(1, 255) + mailingAddressCountry!: string; @IsString() @IsNotEmpty() @@ -160,16 +158,21 @@ export class PantryApplicationDto { @MaxLength(255, { each: true }) restrictions!: string[]; + @ArrayNotEmpty() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + @MaxLength(255, { each: true }) + languages!: string[]; + @IsEnum(RefrigeratedDonation) refrigeratedDonation!: RefrigeratedDonation; @IsBoolean() acceptFoodDeliveries!: boolean; - @IsOptional() @IsString() @IsNotEmpty() - deliveryWindowInstructions?: string; + deliveryWindowInstructions!: string; @IsEnum(ReserveFoodForAllergic) reserveFoodForAllergic!: ReserveFoodForAllergic; @@ -180,24 +183,18 @@ export class PantryApplicationDto { @IsNotEmpty() reservationExplanation?: string; - @IsBoolean() - dedicatedAllergyFriendly!: boolean; + @IsEnum(DedicatedAllergyFriendly) + dedicatedAllergyFriendly!: DedicatedAllergyFriendly; - @IsOptional() @IsEnum(ClientVisitFrequency) - clientVisitFrequency?: ClientVisitFrequency; + clientVisitFrequency!: ClientVisitFrequency; - @IsOptional() - @IsEnum(AllergensConfidence) - identifyAllergensConfidence?: AllergensConfidence; - - @IsOptional() @IsEnum(ServeAllergicChildren) - serveAllergicChildren?: ServeAllergicChildren; + serveAllergicChildren!: ServeAllergicChildren; - @ArrayNotEmpty() + @IsOptional() @IsEnum(Activity, { each: true }) - activities!: Activity[]; + activities?: Activity[]; @IsOptional() @IsString() @@ -214,8 +211,4 @@ export class PantryApplicationDto { @IsNotEmpty() @Length(1, 255) needMoreOptions!: string; - - @IsOptional() - @IsBoolean() - newsletterSubscription?: boolean; } diff --git a/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts b/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts index a5325e1ff..b744ca63b 100644 --- a/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts +++ b/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts @@ -14,7 +14,7 @@ import { RefrigeratedDonation, ReserveFoodForAllergic, ClientVisitFrequency, - AllergensConfidence, + DedicatedAllergyFriendly, ServeAllergicChildren, Activity, } from '../types'; @@ -156,23 +156,25 @@ export class UpdatePantryApplicationDto { @IsString() reservationExplanation?: string | null; - @IsBoolean() @IsOptional() - dedicatedAllergyFriendly?: boolean; + @IsEnum(DedicatedAllergyFriendly) + dedicatedAllergyFriendly?: DedicatedAllergyFriendly; + @ArrayNotEmpty() @IsOptional() - @IsEnum(ClientVisitFrequency) - clientVisitFrequency?: ClientVisitFrequency; + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + @MaxLength(255, { each: true }) + languages?: string[]; @IsOptional() - @IsEnum(AllergensConfidence) - identifyAllergensConfidence?: AllergensConfidence; + @IsEnum(ClientVisitFrequency) + clientVisitFrequency?: ClientVisitFrequency; @IsOptional() @IsEnum(ServeAllergicChildren) serveAllergicChildren?: ServeAllergicChildren; - @ArrayNotEmpty() @IsOptional() @IsEnum(Activity, { each: true }) activities?: Activity[]; @@ -194,8 +196,4 @@ export class UpdatePantryApplicationDto { @IsNotEmpty() @Length(1, 255) needMoreOptions?: string; - - @IsOptional() - @IsBoolean() - newsletterSubscription?: boolean; } diff --git a/apps/backend/src/pantries/pantries.controller.spec.ts b/apps/backend/src/pantries/pantries.controller.spec.ts index bc2c83f7b..6c9adabf6 100644 --- a/apps/backend/src/pantries/pantries.controller.spec.ts +++ b/apps/backend/src/pantries/pantries.controller.spec.ts @@ -7,8 +7,8 @@ import { PantryApplicationDto } from './dtos/pantry-application.dto'; import { OrdersService } from '../orders/order.service'; import { Activity, - AllergensConfidence, ClientVisitFrequency, + DedicatedAllergyFriendly, PantryStats, RefrigeratedDonation, ReserveFoodForAllergic, @@ -69,18 +69,17 @@ describe('PantriesController', () => { pantryName: 'New Community Pantry', allergenClients: '10 to 20', restrictions: ['Peanut allergy', 'Gluten'], + languages: ['English', 'Spanish'], refrigeratedDonation: RefrigeratedDonation.YES, - dedicatedAllergyFriendly: true, + dedicatedAllergyFriendly: DedicatedAllergyFriendly.YES, reserveFoodForAllergic: ReserveFoodForAllergic.SOME, reservationExplanation: 'We have a dedicated allergen-free section', clientVisitFrequency: ClientVisitFrequency.DAILY, - identifyAllergensConfidence: AllergensConfidence.VERY_CONFIDENT, serveAllergicChildren: ServeAllergicChildren.YES_MANY, activities: [Activity.CREATE_LABELED_SHELF, Activity.COLLECT_FEEDBACK], activitiesComments: 'We provide nutritional counseling', itemsInStock: 'Canned goods, pasta', needMoreOptions: 'More fresh produce', - newsletterSubscription: true, } as PantryApplicationDto; // Mock Food Request @@ -308,7 +307,6 @@ describe('PantriesController', () => { secondaryContactEmail: 'john.doe@example.com', refrigeratedDonation: RefrigeratedDonation.NO, reserveFoodForAllergic: ReserveFoodForAllergic.NO, - newsletterSubscription: false, itemsInStock: 'Canned beans, rice', }; diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts index b8286b0d4..24ea262ec 100644 --- a/apps/backend/src/pantries/pantries.controller.ts +++ b/apps/backend/src/pantries/pantries.controller.ts @@ -19,8 +19,8 @@ import { PantryApplicationDto } from './dtos/pantry-application.dto'; import { ApiBody } from '@nestjs/swagger'; import { Activity, - AllergensConfidence, ClientVisitFrequency, + DedicatedAllergyFriendly, PantryStats, RefrigeratedDonation, ReserveFoodForAllergic, @@ -270,6 +270,11 @@ export class PantriesController { items: { type: 'string' }, example: ['Egg allergy', 'Fish allergy'], }, + languages: { + type: 'array', + items: { type: 'string' }, + example: ['English', 'Spanish'], + }, refrigeratedDonation: { type: 'string', enum: Object.values(RefrigeratedDonation), @@ -294,19 +299,15 @@ export class PantriesController { 'We keep a dedicated section for clients with severe allergies', }, dedicatedAllergyFriendly: { - type: 'boolean', - example: true, + type: 'string', + enum: Object.values(DedicatedAllergyFriendly), + example: DedicatedAllergyFriendly.YES, }, clientVisitFrequency: { type: 'string', enum: Object.values(ClientVisitFrequency), example: ClientVisitFrequency.DAILY, }, - identifyAllergensConfidence: { - type: 'string', - enum: Object.values(AllergensConfidence), - example: AllergensConfidence.NOT_VERY_CONFIDENT, - }, serveAllergicChildren: { type: 'string', enum: Object.values(ServeAllergicChildren), @@ -338,10 +339,6 @@ export class PantriesController { maxLength: 255, example: 'Quite often', }, - newsletterSubscription: { - type: 'boolean', - example: true, - }, }, required: [ 'contactFirstName', @@ -359,11 +356,15 @@ export class PantriesController { 'mailingAddressState', 'mailingAddressZip', 'allergenClients', + 'restrictions', + 'languages', 'refrigeratedDonation', 'acceptFoodDeliveries', + 'deliveryWindowInstructions', 'reserveFoodForAllergic', 'dedicatedAllergyFriendly', - 'activities', + 'clientVisitFrequency', + 'serveAllergicChildren', 'itemsInStock', 'needMoreOptions', ], diff --git a/apps/backend/src/pantries/pantries.entity.ts b/apps/backend/src/pantries/pantries.entity.ts index b13c81df5..64bc42ac5 100644 --- a/apps/backend/src/pantries/pantries.entity.ts +++ b/apps/backend/src/pantries/pantries.entity.ts @@ -9,8 +9,8 @@ import { import { User } from '../users/users.entity'; import { Activity, - AllergensConfidence, ClientVisitFrequency, + DedicatedAllergyFriendly, RefrigeratedDonation, ReserveFoodForAllergic, ServeAllergicChildren, @@ -49,9 +49,9 @@ export class Pantry { name: 'shipment_address_country', type: 'varchar', length: 255, - nullable: true, + default: 'US', }) - shipmentAddressCountry!: string | null; + shipmentAddressCountry!: string; @Column({ name: 'mailing_address_line_1', type: 'varchar', length: 255 }) mailingAddressLine1!: string; @@ -77,9 +77,9 @@ export class Pantry { name: 'mailing_address_country', type: 'varchar', length: 255, - nullable: true, + default: 'US', }) - mailingAddressCountry!: string | null; + mailingAddressCountry!: string; @Column({ name: 'allergen_clients', type: 'varchar', length: 25 }) allergenClients!: string; @@ -98,9 +98,8 @@ export class Pantry { @Column({ name: 'delivery_window_instructions', type: 'text', - nullable: true, }) - deliveryWindowInstructions!: string | null; + deliveryWindowInstructions!: string; @Column({ name: 'reserve_food_for_allergic', @@ -115,43 +114,34 @@ export class Pantry { @Column({ name: 'dedicated_allergy_friendly', - type: 'boolean', + type: 'enum', + enum: DedicatedAllergyFriendly, + enumName: 'dedicated_allergy_friendly_enum', }) - dedicatedAllergyFriendly!: boolean; + dedicatedAllergyFriendly!: DedicatedAllergyFriendly; @Column({ name: 'client_visit_frequency', type: 'enum', enum: ClientVisitFrequency, enumName: 'client_visit_frequency_enum', - nullable: true, - }) - clientVisitFrequency!: ClientVisitFrequency | null; - - @Column({ - name: 'identify_allergens_confidence', - type: 'enum', - enum: AllergensConfidence, - enumName: 'allergens_confidence_enum', - nullable: true, }) - identifyAllergensConfidence!: AllergensConfidence | null; + clientVisitFrequency!: ClientVisitFrequency; @Column({ name: 'serve_allergic_children', type: 'enum', enum: ServeAllergicChildren, enumName: 'serve_allergic_children_enum', - nullable: true, }) - serveAllergicChildren!: ServeAllergicChildren | null; - - @Column({ name: 'newsletter_subscription', type: 'boolean', nullable: true }) - newsletterSubscription!: boolean | null; + serveAllergicChildren!: ServeAllergicChildren; @Column({ name: 'restrictions', type: 'text', array: true }) restrictions!: string[]; + @Column({ name: 'languages', type: 'text', array: true }) + languages!: string[]; + @Column({ name: 'has_email_contact', type: 'boolean' }) hasEmailContact!: boolean; @@ -225,8 +215,9 @@ export class Pantry { enum: Activity, enumName: 'activity_enum', array: true, + nullable: true, }) - activities!: Activity[]; + activities!: Activity[] | null; @Column({ name: 'activities_comments', type: 'text', nullable: true }) activitiesComments!: string | null; diff --git a/apps/backend/src/pantries/pantries.service.spec.ts b/apps/backend/src/pantries/pantries.service.spec.ts index 65a267ad6..1ac00cdc2 100644 --- a/apps/backend/src/pantries/pantries.service.spec.ts +++ b/apps/backend/src/pantries/pantries.service.spec.ts @@ -18,7 +18,7 @@ import { ServeAllergicChildren, ReserveFoodForAllergic, Activity, - AllergensConfidence, + DedicatedAllergyFriendly, } from './types'; import { ApplicationStatus } from '../shared/types'; import { testDataSource } from '../config/typeormTestDataSource'; @@ -56,10 +56,14 @@ const makePantryDto = (i: number): PantryApplicationDto => mailingAddressZip: '00000', allergenClients: 'none', restrictions: ['none'], + languages: ['English'], refrigeratedDonation: RefrigeratedDonation.NO, acceptFoodDeliveries: false, + deliveryWindowInstructions: 'none', reserveFoodForAllergic: ReserveFoodForAllergic.NO, - dedicatedAllergyFriendly: false, + dedicatedAllergyFriendly: DedicatedAllergyFriendly.YES, + clientVisitFrequency: ClientVisitFrequency.ONCE_A_WEEK, + serveAllergicChildren: ServeAllergicChildren.NO, activities: [Activity.CREATE_LABELED_SHELF], itemsInStock: 'none', needMoreOptions: 'none', @@ -76,16 +80,22 @@ const dto: PantryApplicationDto = { shipmentAddressCity: 'Testville', shipmentAddressState: 'TX', shipmentAddressZip: '11111', + shipmentAddressCountry: 'US', mailingAddressLine1: '1 Test St', mailingAddressCity: 'Testville', mailingAddressState: 'TX', mailingAddressZip: '11111', + mailingAddressCountry: 'US', allergenClients: 'none', restrictions: ['none'], + languages: ['English'], refrigeratedDonation: RefrigeratedDonation.NO, acceptFoodDeliveries: false, + deliveryWindowInstructions: 'none', reserveFoodForAllergic: ReserveFoodForAllergic.NO, - dedicatedAllergyFriendly: false, + dedicatedAllergyFriendly: DedicatedAllergyFriendly.YES, + clientVisitFrequency: ClientVisitFrequency.ONCE_A_WEEK, + serveAllergicChildren: ServeAllergicChildren.NO, activities: [Activity.CREATE_LABELED_SHELF], itemsInStock: 'none', needMoreOptions: 'none', @@ -310,13 +320,11 @@ describe('PantriesService', () => { deliveryWindowInstructions: 'Weekdays 9am-5pm', reserveFoodForAllergic: ReserveFoodForAllergic.SOME, reservationExplanation: 'We have a dedicated section', - dedicatedAllergyFriendly: true, + dedicatedAllergyFriendly: DedicatedAllergyFriendly.YES, clientVisitFrequency: ClientVisitFrequency.DAILY, - identifyAllergensConfidence: AllergensConfidence.VERY_CONFIDENT, serveAllergicChildren: ServeAllergicChildren.YES_MANY, activities: [Activity.CREATE_LABELED_SHELF, Activity.COLLECT_FEEDBACK], activitiesComments: 'We are committed to allergen management', - newsletterSubscription: true, }; await service.addPantry(optionalDto); @@ -398,7 +406,6 @@ describe('PantriesService', () => { secondaryContactLastName: 'Doe', refrigeratedDonation: RefrigeratedDonation.YES, reserveFoodForAllergic: ReserveFoodForAllergic.SOME, - newsletterSubscription: true, itemsInStock: 'Canned beans, rice', }; @@ -409,7 +416,6 @@ describe('PantriesService', () => { expect(updatedPantry.reserveFoodForAllergic).toBe( ReserveFoodForAllergic.SOME, ); - expect(updatedPantry.newsletterSubscription).toBe(true); expect(updatedPantry.itemsInStock).toBe('Canned beans, rice'); }); @@ -933,11 +939,15 @@ describe('PantriesService', () => { restrictions: ['none'], refrigeratedDonation: RefrigeratedDonation.NO, acceptFoodDeliveries: false, + deliveryWindowInstructions: 'none', reserveFoodForAllergic: ReserveFoodForAllergic.NO, - dedicatedAllergyFriendly: false, + dedicatedAllergyFriendly: DedicatedAllergyFriendly.YES, + clientVisitFrequency: ClientVisitFrequency.ONCE_A_WEEK, + serveAllergicChildren: ServeAllergicChildren.NO, activities: [Activity.CREATE_LABELED_SHELF], itemsInStock: 'none', needMoreOptions: 'none', + languages: ['English'], } as PantryApplicationDto); const saved = await testDataSource.getRepository(Pantry).findOne({ diff --git a/apps/backend/src/pantries/pantries.service.ts b/apps/backend/src/pantries/pantries.service.ts index cb24b5119..66799718f 100644 --- a/apps/backend/src/pantries/pantries.service.ts +++ b/apps/backend/src/pantries/pantries.service.ts @@ -313,7 +313,7 @@ export class PantriesService { pantry.shipmentAddressCity = pantryData.shipmentAddressCity; pantry.shipmentAddressState = pantryData.shipmentAddressState; pantry.shipmentAddressZip = pantryData.shipmentAddressZip; - pantry.shipmentAddressCountry = pantryData.shipmentAddressCountry ?? null; + pantry.shipmentAddressCountry = pantryData.shipmentAddressCountry; // mailing address information pantry.mailingAddressLine1 = pantryData.mailingAddressLine1; @@ -321,25 +321,25 @@ export class PantriesService { pantry.mailingAddressCity = pantryData.mailingAddressCity; pantry.mailingAddressState = pantryData.mailingAddressState; pantry.mailingAddressZip = pantryData.mailingAddressZip; - pantry.mailingAddressCountry = pantryData.mailingAddressCountry ?? null; + pantry.mailingAddressCountry = pantryData.mailingAddressCountry; // pantry details information pantry.pantryName = pantryData.pantryName; pantry.allergenClients = pantryData.allergenClients; pantry.restrictions = pantryData.restrictions; + pantry.languages = pantryData.languages; pantry.refrigeratedDonation = pantryData.refrigeratedDonation; + pantry.acceptFoodDeliveries = pantryData.acceptFoodDeliveries; + pantry.deliveryWindowInstructions = pantryData.deliveryWindowInstructions; pantry.dedicatedAllergyFriendly = pantryData.dedicatedAllergyFriendly; pantry.reserveFoodForAllergic = pantryData.reserveFoodForAllergic; pantry.reservationExplanation = pantryData.reservationExplanation ?? null; - pantry.clientVisitFrequency = pantryData.clientVisitFrequency ?? null; - pantry.identifyAllergensConfidence = - pantryData.identifyAllergensConfidence ?? null; - pantry.serveAllergicChildren = pantryData.serveAllergicChildren ?? null; - pantry.activities = pantryData.activities; + pantry.clientVisitFrequency = pantryData.clientVisitFrequency; + pantry.serveAllergicChildren = pantryData.serveAllergicChildren; + pantry.activities = pantryData.activities ?? null; pantry.activitiesComments = pantryData.activitiesComments ?? null; pantry.itemsInStock = pantryData.itemsInStock; pantry.needMoreOptions = pantryData.needMoreOptions; - pantry.newsletterSubscription = pantryData.newsletterSubscription ?? null; // pantry contact is automatically added to User table await this.repo.save(pantry); diff --git a/apps/backend/src/pantries/types.ts b/apps/backend/src/pantries/types.ts index 8b904d331..543f6883c 100644 --- a/apps/backend/src/pantries/types.ts +++ b/apps/backend/src/pantries/types.ts @@ -55,22 +55,21 @@ export enum ClientVisitFrequency { ONCE_A_MONTH = 'Once a month', } -export enum AllergensConfidence { - VERY_CONFIDENT = 'Very confident', - SOMEWHAT_CONFIDENT = 'Somewhat confident', - NOT_VERY_CONFIDENT = 'Not very confident (we need more education!)', -} - export enum ServeAllergicChildren { YES_MANY = 'Yes, many (> 10)', YES_FEW = 'Yes, a few (< 10)', NO = 'No', } +export enum DedicatedAllergyFriendly { + YES = 'Yes', + NO_BUT_CAN_ACCOMMODATE = 'No but we can accommodate this', + NO_CANNOT_ACCOMMODATE = 'No, and we cannot accommodate this', +} + export enum Activity { CREATE_LABELED_SHELF = 'Create labeled shelf', PROVIDE_EDUCATIONAL_PAMPHLETS = 'Provide educational pamphlets', - TRACK_DIETARY_NEEDS = 'Spreadsheet to track dietary needs', POST_RESOURCE_FLYERS = 'Post allergen-free resource flyers', SURVEY_CLIENTS = 'Survey clients to determine medical dietary needs', COLLECT_FEEDBACK = 'Collect feedback from allergen-avoidant clients', diff --git a/apps/frontend/src/components/forms/editableFMApplication.tsx b/apps/frontend/src/components/forms/editableFMApplication.tsx index a82f61a4c..361594f59 100644 --- a/apps/frontend/src/components/forms/editableFMApplication.tsx +++ b/apps/frontend/src/components/forms/editableFMApplication.tsx @@ -15,11 +15,7 @@ import { FoodManufacturer, UpdateFoodManufacturerApplicationDto, } from '../../types/types'; -import { - Allergen, - DonateWastedFood, - ManufacturerAttribute, -} from '../../types/manufacturerEnums'; +import { Allergen, DonateWastedFood } from '../../types/manufacturerEnums'; import { formatPhone } from '@utils/utils'; import { TagGroup } from '@components/forms/tagGroup'; import { USPhoneInput } from '@components/forms/usPhoneInput'; @@ -31,13 +27,11 @@ import { Field, EditField, EditRadio, - EditSelect, EditMultiSelect, } from '@components/editableComponents'; const allergenOptions = Object.values(Allergen); const donateWastedFoodOptions = Object.values(DonateWastedFood); -const manufacturerAttributeOptions = Object.values(ManufacturerAttribute); type FormState = { secondaryContactFirstName: string; @@ -49,13 +43,10 @@ type FormState = { unlistedProductAllergens: string[]; facilityFreeAllergens: string[]; productsGlutenFree: string; - productsContainSulfites: string; productsSustainableExplanation: string; inKindDonations: string; donateWastedFood: string; - manufacturerAttribute: string; additionalComments: string; - newsletterSubscription: string; }; function buildFormState(app: FoodManufacturer): FormState { @@ -69,18 +60,10 @@ function buildFormState(app: FoodManufacturer): FormState { unlistedProductAllergens: app.unlistedProductAllergens ?? [], facilityFreeAllergens: app.facilityFreeAllergens ?? [], productsGlutenFree: app.productsGlutenFree ? 'Yes, always' : 'No', - productsContainSulfites: app.productsContainSulfites ? 'Yes' : 'No', productsSustainableExplanation: app.productsSustainableExplanation ?? '', inKindDonations: app.inKindDonations ? 'Yes' : 'No', donateWastedFood: app.donateWastedFood ?? '', - manufacturerAttribute: app.manufacturerAttribute ?? '', additionalComments: app.additionalComments ?? '', - newsletterSubscription: - app.newsletterSubscription != null - ? app.newsletterSubscription - ? 'Yes' - : 'No' - : '', }; } @@ -91,7 +74,6 @@ function validateRequired(form: FormState): boolean { form.unlistedProductAllergens.length > 0 && form.facilityFreeAllergens.length > 0 && !!form.productsGlutenFree && - !!form.productsContainSulfites && !!form.productsSustainableExplanation.trim() && !!form.inKindDonations && !!form.donateWastedFood @@ -161,18 +143,12 @@ const EditableFMApplication: React.FC = ({ unlistedProductAllergens: form.unlistedProductAllergens as Allergen[], facilityFreeAllergens: form.facilityFreeAllergens as Allergen[], productsGlutenFree: form.productsGlutenFree === 'Yes, always', - productsContainSulfites: form.productsContainSulfites === 'Yes', productsSustainableExplanation: form.productsSustainableExplanation || undefined, inKindDonations: form.inKindDonations === 'Yes', donateWastedFood: (form.donateWastedFood as DonateWastedFood) || undefined, - manufacturerAttribute: - (form.manufacturerAttribute as ManufacturerAttribute) || undefined, additionalComments: form.additionalComments || undefined, - newsletterSubscription: form.newsletterSubscription - ? form.newsletterSubscription === 'Yes' - : undefined, }; const updated = await ApiClient.updateFoodManufacturerApplicationData( application.foodManufacturerId, @@ -291,10 +267,6 @@ const EditableFMApplication: React.FC = ({ label="Are products certified gluten-free" value={application.productsGlutenFree ? 'Yes, always' : 'No'} /> - = ({ label="Are your products sustainable or environmentally conscious?" value={application.productsSustainableExplanation} /> - - ); @@ -430,15 +388,6 @@ const EditableFMApplication: React.FC = ({ required /> - setField('productsContainSulfites', v)} - required - /> - = ({ required /> - setField('manufacturerAttribute', v)} - /> - = ({ textarea /> - setField('newsletterSubscription', v)} - /> - {error && ( {error} diff --git a/apps/frontend/src/components/forms/editablePantryApplication.tsx b/apps/frontend/src/components/forms/editablePantryApplication.tsx index 8fb74e023..91c83a9b7 100644 --- a/apps/frontend/src/components/forms/editablePantryApplication.tsx +++ b/apps/frontend/src/components/forms/editablePantryApplication.tsx @@ -17,13 +17,18 @@ import { RefrigeratedDonation, ReserveFoodForAllergic, ClientVisitFrequency, - AllergensConfidence, ServeAllergicChildren, + DedicatedAllergyFriendly, } from '../../types/pantryEnums'; import { formatPhone } from '@utils/utils'; import { TagGroup } from '@components/forms/tagGroup'; import { USPhoneInput } from '@components/forms/usPhoneInput'; -import { dietaryRestrictionOptions } from '@components/forms/pantryApplicationForm'; +import { + dietaryRestrictionOptions, + restrictionsOtherOption, + languageOptions, + languageOtherOption, +} from '@components/forms/pantryApplicationForm'; import { fieldHeaderStyles, fieldContentStyles, @@ -39,15 +44,21 @@ import { import { AuthError } from 'aws-amplify/auth'; const allergenClientOptions = [ - '< 10', + 'Less than 10', '10 to 20', '20 to 50', '50 to 100', - '> 100', + 'Greater than 100', "I'm not sure", 'I have an exact number', ]; +// The application form no longer offers "Something else", so it is excluded +// from the options a pantry can pick when editing. +const activityEditOptions = Object.values(Activity).filter( + (activity) => activity !== Activity.SOMETHING_ELSE, +); + interface AddressSectionProps { title: string; line1?: string | null; @@ -103,6 +114,9 @@ type FormState = { acceptFoodDeliveries: string; deliveryWindowInstructions: string; restrictions: string[]; + restrictionsOther: string; + languages: string[]; + languagesOther: string; allergenClients: string; allergenClientsExact: string; refrigeratedDonation: string; @@ -110,21 +124,45 @@ type FormState = { reservationExplanation: string; dedicatedAllergyFriendly: string; clientVisitFrequency: string; - identifyAllergensConfidence: string; serveAllergicChildren: string; activities: string[]; activitiesComments: string; itemsInStock: string; needMoreOptions: string; - newsletterSubscription: string; }; +// Splits a stored multiselect array into the values that match preset options +// and any free-text "other" values. If an other value exists but the "Other" +// option isn't already selected, it is added so the specify input shows. +function splitOther( + stored: string[], + options: string[], + otherOption: string, +): { selected: string[]; other: string } { + const selected = stored.filter((value) => options.includes(value)); + const custom = stored.filter((value) => !options.includes(value)); + if (custom.length > 0 && !selected.includes(otherOption)) { + selected.push(otherOption); + } + return { selected, other: custom.join(', ') }; +} + function buildFormState(app: PantryWithUser): FormState { // If allergenClients is not one of the dropdown options it was entered as an exact number const storedAllergenClients = app.allergenClients ?? ''; const isStandardAllergenOption = allergenClientOptions.includes( storedAllergenClients, ); + const restrictions = splitOther( + app.restrictions ?? [], + dietaryRestrictionOptions, + restrictionsOtherOption, + ); + const languages = splitOther( + app.languages ?? [], + languageOptions, + languageOtherOption, + ); return { secondaryContactFirstName: app.secondaryContactFirstName ?? '', secondaryContactLastName: app.secondaryContactLastName ?? '', @@ -144,7 +182,10 @@ function buildFormState(app: PantryWithUser): FormState { mailingCountry: app.mailingAddressCountry ?? '', acceptFoodDeliveries: app.acceptFoodDeliveries ? 'Yes' : 'No', deliveryWindowInstructions: app.deliveryWindowInstructions ?? '', - restrictions: app.restrictions ?? [], + restrictions: restrictions.selected, + restrictionsOther: restrictions.other, + languages: languages.selected, + languagesOther: languages.other, allergenClients: isStandardAllergenOption ? storedAllergenClients : 'I have an exact number', @@ -152,20 +193,13 @@ function buildFormState(app: PantryWithUser): FormState { refrigeratedDonation: app.refrigeratedDonation ?? '', reserveFoodForAllergic: app.reserveFoodForAllergic ?? '', reservationExplanation: app.reservationExplanation ?? '', - dedicatedAllergyFriendly: app.dedicatedAllergyFriendly ? 'Yes' : 'No', + dedicatedAllergyFriendly: app.dedicatedAllergyFriendly ?? '', clientVisitFrequency: app.clientVisitFrequency ?? '', - identifyAllergensConfidence: app.identifyAllergensConfidence ?? '', serveAllergicChildren: app.serveAllergicChildren ?? '', activities: app.activities ?? [], activitiesComments: app.activitiesComments ?? '', itemsInStock: app.itemsInStock ?? '', needMoreOptions: app.needMoreOptions ?? '', - newsletterSubscription: - app.newsletterSubscription != null - ? app.newsletterSubscription - ? 'Yes' - : 'No' - : '', }; } @@ -180,12 +214,17 @@ function validateRequired(form: FormState): boolean { !!form.mailingState.trim() && !!form.mailingZip.trim() && !!form.acceptFoodDeliveries && + !!form.deliveryWindowInstructions.trim() && !!form.allergenClients && !( form.allergenClients === 'I have an exact number' && !form.allergenClientsExact.trim() ) && form.restrictions.length > 0 && + !( + form.restrictions.includes(restrictionsOtherOption) && + !form.restrictionsOther.trim() + ) && !!form.refrigeratedDonation && !!form.dedicatedAllergyFriendly && !!form.reserveFoodForAllergic && @@ -194,10 +233,12 @@ function validateRequired(form: FormState): boolean { form.reserveFoodForAllergic === ReserveFoodForAllergic.SOME) && !form.reservationExplanation.trim() ) && - form.activities.length > 0 && + !!form.clientVisitFrequency && + !!form.serveAllergicChildren && + form.languages.length > 0 && !( - form.activities.includes(Activity.SOMETHING_ELSE) && - !form.activitiesComments.trim() + form.languages.includes(languageOtherOption) && + !form.languagesOther.trim() ) && !!form.itemsInStock.trim() && !!form.needMoreOptions.trim() @@ -258,6 +299,23 @@ const EditablePantryApplication: React.FC = ({ setIsSaving(true); setError(null); try { + const restrictions = [...form.restrictions]; + if ( + form.restrictions.includes(restrictionsOtherOption) && + form.restrictionsOther.trim() + ) { + restrictions.push(form.restrictionsOther.trim()); + } + const languages = form.languages.filter( + (language) => language !== languageOtherOption, + ); + if ( + form.languages.includes(languageOtherOption) && + form.languagesOther.trim() + ) { + languages.push(form.languagesOther.trim()); + } + const formData: UpdatePantryApplicationDto = { secondaryContactFirstName: form.secondaryContactFirstName || undefined, secondaryContactLastName: form.secondaryContactLastName || undefined, @@ -282,10 +340,13 @@ const EditablePantryApplication: React.FC = ({ form.allergenClients === 'I have an exact number' ? form.allergenClientsExact || undefined : form.allergenClients || undefined, - restrictions: form.restrictions, + restrictions, + languages, refrigeratedDonation: (form.refrigeratedDonation as RefrigeratedDonation) || undefined, - dedicatedAllergyFriendly: form.dedicatedAllergyFriendly === 'Yes', + dedicatedAllergyFriendly: + (form.dedicatedAllergyFriendly as DedicatedAllergyFriendly) || + undefined, reserveFoodForAllergic: (form.reserveFoodForAllergic as ReserveFoodForAllergic) || undefined, reservationExplanation: @@ -295,16 +356,12 @@ const EditablePantryApplication: React.FC = ({ : null, clientVisitFrequency: (form.clientVisitFrequency as ClientVisitFrequency) || undefined, - identifyAllergensConfidence: - (form.identifyAllergensConfidence as AllergensConfidence) || - undefined, serveAllergicChildren: (form.serveAllergicChildren as ServeAllergicChildren) || undefined, activities: form.activities as Activity[], activitiesComments: form.activitiesComments || undefined, itemsInStock: form.itemsInStock || undefined, needMoreOptions: form.needMoreOptions || undefined, - newsletterSubscription: form.newsletterSubscription === 'Yes', }; const updated = await ApiClient.updatePantryApplicationData( application.pantryId, @@ -410,14 +467,6 @@ const EditablePantryApplication: React.FC = ({ } /> - - = ({ country={application.mailingAddressCountry} /> +
+ + +
+
+ + - Food Allergies and Restrictions + + Food allergies / dietary restrictions clients report + {application.restrictions?.length ? ( ) : ( @@ -442,43 +509,48 @@ const EditablePantryApplication: React.FC = ({ - - - Activities with SSF + + Languages allergen-avoidant clients speak + + {application.languages?.length ? ( + + ) : ( + - + )} + + + + + Activities open to doing with SSF + {application.activities?.length ? ( ) : ( @@ -487,23 +559,16 @@ const EditablePantryApplication: React.FC = ({ -
@@ -560,6 +625,14 @@ const EditablePantryApplication: React.FC = ({ requiredSuffixes={['Line1', 'City', 'State', 'Zip']} /> + + = ({ value={form.deliveryWindowInstructions} onChange={(v) => setField('deliveryWindowInstructions', v)} textarea - /> - - Pantry Details = ({ required /> + {form.restrictions.includes(restrictionsOtherOption) && ( + setField('restrictionsOther', v)} + required + /> + )} + = ({ label="Do you have a dedicated shelf or section of your pantry for allergy-friendly items?" name="dedicatedAllergyFriendly" value={form.dedicatedAllergyFriendly} - options={['Yes', 'No']} + options={Object.values(DedicatedAllergyFriendly)} onChange={(v) => setField('dedicatedAllergyFriendly', v)} required /> @@ -667,46 +743,56 @@ const EditablePantryApplication: React.FC = ({ value={form.clientVisitFrequency} options={Object.values(ClientVisitFrequency)} onChange={(v) => setField('clientVisitFrequency', v)} + required /> setField('identifyAllergensConfidence', v)} - /> - - setField('serveAllergicChildren', v)} + required + /> + + + setForm((prev) => (prev ? { ...prev, languages: v } : prev)) + } + triggerLabel="Select languages" + required /> + {form.languages.includes(languageOtherOption) && ( + setField('languagesOther', v)} + required + /> + )} + setForm((prev) => (prev ? { ...prev, activities: v } : prev)) } triggerLabel="Select activities" - helperText="Food donations are one part of being a partner pantry. The following are additional ways to help us better support you! Please select all that apply." - required + helperText="Check all that apply." /> setField('activitiesComments', v)} textarea - required={form.activities.includes(Activity.SOMETHING_ELSE)} - helperText='If you answered "Something Else," please elaborate.' /> = ({ /> setField('needMoreOptions', v)} textarea required - /> - - setField('newsletterSubscription', v)} + helperText="Please share any feedback you have received." /> {error && ( diff --git a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx index e104581cf..a497f008b 100644 --- a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx +++ b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx @@ -9,11 +9,10 @@ import { Field, Textarea, SimpleGrid, - NativeSelect, - NativeSelectIndicator, Separator, Checkbox, Menu, + Link, } from '@chakra-ui/react'; import { ActionFunction, @@ -29,11 +28,7 @@ import { ManufacturerApplicationDto } from '../../types/types'; import ApiClient from '@api/apiClient'; import axios from 'axios'; import { ChevronDownIcon } from 'lucide-react'; -import { - Allergen, - DonateWastedFood, - ManufacturerAttribute, -} from '../../types/manufacturerEnums'; +import { Allergen, DonateWastedFood } from '../../types/manufacturerEnums'; import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../../hooks/alert'; import { ROUTES } from '../../routes'; @@ -81,7 +76,7 @@ const ManufacturerApplicationForm: React.FC = () => { }, [actionData, setAlertMessage]); return ( - + {alertState && ( { Product Details - What allergen(s) are not listed in your products' ingredients? + Which allergen(s) are not listed in your products' ingredients? @@ -361,7 +356,7 @@ const ManufacturerApplicationForm: React.FC = () => { - What allergen(s) is your facility free from? + Which allergen(s) is your facility free from? @@ -461,9 +456,9 @@ const ManufacturerApplicationForm: React.FC = () => { /> - + - Are your products certified gluten-free? + Do your products have gluten-free certification? @@ -486,31 +481,6 @@ const ManufacturerApplicationForm: React.FC = () => { - - - Do your products contain sulfites? - - - - - {['Yes', 'No'].map((value) => ( - - - - - - - {value} - - - ))} - - - - Additional Details @@ -582,26 +552,6 @@ const ManufacturerApplicationForm: React.FC = () => { - - Are you: - - - {Object.values(ManufacturerAttribute).map((value) => ( - - ))} - - - - - Anything else we should know? @@ -615,40 +565,53 @@ const ManufacturerApplicationForm: React.FC = () => { - - - Would you like to subscribe to our quarterly newsletter? - - - - {['Yes', 'No'].map((value) => ( - - - - - - - {value} - - - ))} - - + + + + + + By submitting this form, you agree to our{' '} + e.stopPropagation()} + > + Privacy Policy + {' '} + and{' '} + e.stopPropagation()} + > + Terms of Use + + . + + - + - By submitting this form, you agree to our Privacy Policy.{' '} - + By submitting this form, you agree to receive automated emails + from Securing Safe Food (SSF) Corp. should your pantry be + enrolled in our program. @@ -690,22 +653,12 @@ export const submitManufacturerApplicationForm: ActionFunction = async ({ 'productsGlutenFree', form.get('productsGlutenFree') === 'Yes, always', ); - manufacturerApplicationData.set( - 'productsContainSulfites', - form.get('productsContainSulfites') === 'Yes', - ); manufacturerApplicationData.set( 'inKindDonations', form.get('inKindDonations') === 'Yes', ); - manufacturerApplicationData.set( - 'newsletterSubscription', - form.get('newsletterSubscription') === 'Yes', - ); form.delete('productsGlutenFree'); - form.delete('productsContainSulfites'); form.delete('inKindDonations'); - form.delete('newsletterSubscription'); form.delete('unlistedProductAllergens'); form.delete('facilityFreeAllergens'); diff --git a/apps/frontend/src/components/forms/pantryApplicationForm.tsx b/apps/frontend/src/components/forms/pantryApplicationForm.tsx index 526de1d1d..2be1bd447 100644 --- a/apps/frontend/src/components/forms/pantryApplicationForm.tsx +++ b/apps/frontend/src/components/forms/pantryApplicationForm.tsx @@ -14,6 +14,7 @@ import { Separator, Checkbox, Menu, + Link, } from '@chakra-ui/react'; import { ActionFunction, @@ -26,7 +27,7 @@ import React, { useEffect, useState } from 'react'; import { USPhoneInput } from '@components/forms/usPhoneInput'; import { PantryApplicationDto } from '../../types/types'; import ApiClient from '@api/apiClient'; -import { Activity } from '../../types/pantryEnums'; +import { Activity, DedicatedAllergyFriendly } from '../../types/pantryEnums'; import axios from 'axios'; import { ChevronDownIcon } from 'lucide-react'; import { TagGroup } from './tagGroup'; @@ -34,38 +35,42 @@ import { FloatingAlert } from '@components/floatingAlert'; import { useAlert } from '../../hooks/alert'; import { ROUTES } from '../../routes'; -export const otherRestrictionsOptions: string[] = [ - 'Other allergy (e.g., yeast, sunflower, etc.)', - 'Other allergic illness (e.g., eosinophilic esophagitis, FPIES, oral allergy syndrome)', - 'Other dietary restriction', -]; +export const restrictionsOtherOption = + "Other (e.g., irritable bowel syndrome, Crohn's disease, fruit/vegetable sensitivities)"; export const dietaryRestrictionOptions = [ - 'Egg allergy', - 'Fish allergy', 'Milk allergy', - 'Lactose intolerance/dairy sensitivity', + 'Lactose intolerance', + 'Egg allergy', 'Peanut allergy', + 'Tree nut allergy', + 'Fish allergy', 'Shellfish allergy', + 'Celiac disease', + 'Gluten intolerance or sensitivity', + 'Wheat allergy', 'Soy allergy', 'Sesame allergy', - 'Tree nut allergy', - 'Wheat allergy', - 'Celiac disease', - 'Gluten sensitivity (not celiac disease)', - "Gastrointestinal illness (IBS, Crohn's, gastroparesis, etc.)", - ...otherRestrictionsOptions, - 'Unsure', + "I'm not sure", + restrictionsOtherOption, ]; export const activityOptions = [ 'Create a labeled, allergy-friendly shelf or shelves', 'Provide clients and staff/volunteers with educational pamphlets', - "Use a spreadsheet to track clients' medical dietary needs and distribution of SSF items per month", 'Post allergen-free resource flyers throughout pantry', 'Survey your clients to determine their medical dietary needs', 'Collect feedback from allergen-avoidant clients on SSF foods', - 'Something else', +]; + +export const languageOtherOption = 'Other (please specify)'; + +export const languageOptions = [ + 'English', + 'Spanish', + 'Mandarin', + 'Russian', + languageOtherOption, ]; const PantryApplicationForm: React.FC = () => { @@ -77,6 +82,7 @@ const PantryApplicationForm: React.FC = () => { const [allergenClients, setAllergenClients] = useState(); const [restrictions, setRestrictions] = useState([]); + const [languages, setLanguages] = useState([]); const [reserveFoodForAllergic, setReserveFoodForAllergic] = useState(); const [differentMailingAddress, setDifferentMailingAddress] = useState< @@ -116,7 +122,7 @@ const PantryApplicationForm: React.FC = () => { }, [actionData, setAlertMessage]); return ( - + {alertState && ( { - Primary Contact Information + + + + Food Pantry Name + + + + + + + Primary Contact + @@ -207,7 +224,7 @@ const PantryApplicationForm: React.FC = () => { - Email Address + Email { + + If you selected "Other," please specify: + + { /> + Secondary Contact - - Secondary Contact Information First Name @@ -292,7 +312,7 @@ const PantryApplicationForm: React.FC = () => { /> - Email Address + Email { - - Food Shipment Address - - - Please list your address for food shipments. + + What is your pantry's address for food shipments? + @@ -331,7 +349,7 @@ const PantryApplicationForm: React.FC = () => { - City/Town + City { - State/Region/Province + Zip Code - Zip/Post Code + Country - - Country + + + State + + + Does this address differ from your pantry's mailing address for - documents? + documents (e.g., 11" by 17" poster resources)?{' '} + { - - - Would your pantry be able to accept food deliveries during - standard business hours Mon-Fri?{' '} - - - - - {['Yes', 'No'].map((value) => ( - - - - - - - {value} - - - ))} - - - - - - Please note any delivery window restrictions. - -