diff --git a/apps/backend/src/auth/auth.controller.ts b/apps/backend/src/auth/auth.controller.ts index 7f603f00..ec741008 100644 --- a/apps/backend/src/auth/auth.controller.ts +++ b/apps/backend/src/auth/auth.controller.ts @@ -34,7 +34,7 @@ export class AuthController { signUpDto.firstName, signUpDto.lastName, signUpDto.phone, - Role.STANDARD_VOLUNTEER, + Role.VOLUNTEER, ); return user; diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts index 2d56324c..a0bae3ad 100644 --- a/apps/backend/src/auth/auth.service.ts +++ b/apps/backend/src/auth/auth.service.ts @@ -60,7 +60,7 @@ export class AuthService { async signup( { firstName, lastName, email, password }: SignUpDto, - role: Role = Role.STANDARD_VOLUNTEER, + role: Role = Role.VOLUNTEER, ): Promise { // Needs error handling const signUpCommand = new SignUpCommand({ diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index 80f6bf29..e1a76d30 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -23,6 +23,7 @@ import { RemoveOrdersDonationId1761500262238 } from '../migrations/1761500262238 import { AddVolunteerPantryUniqueConstraint1760033134668 } from '../migrations/1760033134668-AddVolunteerPantryUniqueConstraint'; import { AllergyFriendlyToBoolType1763963056712 } from '../migrations/1763963056712-AllergyFriendlyToBoolType'; import { UpdatePantryUserFieldsFixed1764350314832 } from '../migrations/1764350314832-UpdatePantryUserFieldsFixed'; +import { RemoveMultipleVolunteerTypes1764811878152 } from '../migrations/1764811878152-RemoveMultipleVolunteerTypes'; import { RemoveUnusedStatuses1764816885341 } from '../migrations/1764816885341-RemoveUnusedStatuses'; import { UpdatePantryFields1763762628431 } from '../migrations/1763762628431-UpdatePantryFields'; @@ -62,6 +63,7 @@ const config = { AddVolunteerPantryUniqueConstraint1760033134668, AllergyFriendlyToBoolType1763963056712, UpdatePantryUserFieldsFixed1764350314832, + RemoveMultipleVolunteerTypes1764811878152, RemoveUnusedStatuses1764816885341, ], }; diff --git a/apps/backend/src/migrations/1764811878152-RemoveMultipleVolunteerTypes.ts b/apps/backend/src/migrations/1764811878152-RemoveMultipleVolunteerTypes.ts new file mode 100644 index 00000000..ae661a95 --- /dev/null +++ b/apps/backend/src/migrations/1764811878152-RemoveMultipleVolunteerTypes.ts @@ -0,0 +1,74 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveMultipleVolunteerTypes1764811878152 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE users + ALTER COLUMN role DROP DEFAULT; + + CREATE TYPE users_role_enum_new AS ENUM ( + 'admin', + 'volunteer', + 'pantry', + 'food_manufacturer' + ); + + ALTER TABLE users + ALTER COLUMN role + TYPE users_role_enum_new + USING ( + CASE + WHEN role IN ('standard_volunteer', 'lead_volunteer') + THEN 'volunteer' + ELSE role::text + END + )::users_role_enum_new; + + DROP TYPE users_role_enum; + + ALTER TYPE users_role_enum_new + RENAME TO users_role_enum; + + ALTER TABLE users + ALTER COLUMN role + SET DEFAULT 'volunteer'; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE users + ALTER COLUMN role DROP DEFAULT; + + CREATE TYPE users_role_enum_old AS ENUM ( + 'admin', + 'lead_volunteer', + 'standard_volunteer', + 'pantry', + 'food_manufacturer' + ); + + ALTER TABLE users + ALTER COLUMN role + TYPE users_role_enum_old + USING ( + CASE + WHEN role = 'volunteer' + THEN 'standard_volunteer' + ELSE role::text + END + )::users_role_enum_old; + + DROP TYPE users_role_enum; + + ALTER TYPE users_role_enum_old + RENAME TO users_role_enum; + + ALTER TABLE users + ALTER COLUMN role + SET DEFAULT 'standard_volunteer'; + `); + } +} diff --git a/apps/backend/src/migrations/1764816885341-RemoveUnusedStatuses.ts b/apps/backend/src/migrations/1764816885341-RemoveUnusedStatuses.ts index 6419e233..4c35fd0f 100644 --- a/apps/backend/src/migrations/1764816885341-RemoveUnusedStatuses.ts +++ b/apps/backend/src/migrations/1764816885341-RemoveUnusedStatuses.ts @@ -1,25 +1,24 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; +import { MigrationInterface, QueryRunner } from 'typeorm'; export class RemoveUnusedStatuses1764816885341 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE allocations DROP COLUMN IF EXISTS status;`, + ); + await queryRunner.query( + `ALTER TABLE donation_items DROP COLUMN IF EXISTS status;`, + ); + } - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE allocations DROP COLUMN IF EXISTS status;` - ); - await queryRunner.query( - `ALTER TABLE donation_items DROP COLUMN IF EXISTS status;` - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` ALTER TABLE allocations ADD COLUMN status VARCHAR(25) NOT NULL DEFAULT 'pending'; `); - await queryRunner.query(` + await queryRunner.query(` ALTER TABLE donation_items ADD COLUMN status VARCHAR(25) NOT NULL DEFAULT 'available'; `); - } + } } diff --git a/apps/backend/src/pantries/dtos/pantry-application.dto.ts b/apps/backend/src/pantries/dtos/pantry-application.dto.ts index 42510915..c7473b0f 100644 --- a/apps/backend/src/pantries/dtos/pantry-application.dto.ts +++ b/apps/backend/src/pantries/dtos/pantry-application.dto.ts @@ -52,7 +52,7 @@ export class PantryApplicationDto { @IsNotEmpty() @MaxLength(255) emailContactOther?: string; - + @IsOptional() @IsString() @IsNotEmpty() diff --git a/apps/backend/src/users/types.ts b/apps/backend/src/users/types.ts index 4eb7f295..695cbc44 100644 --- a/apps/backend/src/users/types.ts +++ b/apps/backend/src/users/types.ts @@ -1,14 +1,6 @@ export enum Role { ADMIN = 'admin', - LEAD_VOLUNTEER = 'lead_volunteer', - STANDARD_VOLUNTEER = 'standard_volunteer', + VOLUNTEER = 'volunteer', PANTRY = 'pantry', FOODMANUFACTURER = 'food_manufacturer', } - -export const VOLUNTEER_ROLES: Role[] = [ - Role.LEAD_VOLUNTEER, - Role.STANDARD_VOLUNTEER, -]; - -export type VolunteerType = (typeof VOLUNTEER_ROLES)[number]; diff --git a/apps/backend/src/users/user.entity.ts b/apps/backend/src/users/user.entity.ts index e2b9a958..746484ce 100644 --- a/apps/backend/src/users/user.entity.ts +++ b/apps/backend/src/users/user.entity.ts @@ -19,7 +19,7 @@ export class User { name: 'role', enum: Role, enumName: 'users_role_enum', - default: Role.STANDARD_VOLUNTEER, + default: Role.VOLUNTEER, }) role: Role; diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 8a22cf3a..97811a0e 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -13,17 +13,25 @@ const mockUserService = mock(); const mockUser1: Partial = { id: 1, - role: Role.STANDARD_VOLUNTEER, + email: 'john@example.com', + firstName: 'John', + lastName: 'Doe', + phone: '1234567890', + role: Role.VOLUNTEER, }; const mockUser2: Partial = { id: 2543210, - role: Role.LEAD_VOLUNTEER, + email: 'bobsmith@example.com', + firstName: 'Bob', + lastName: 'Smith', + phone: '9876', + role: Role.VOLUNTEER, }; const mockUser3: Partial = { id: 3, - role: Role.STANDARD_VOLUNTEER, + role: Role.VOLUNTEER, }; const mockPantries: Partial[] = [ @@ -68,6 +76,45 @@ describe('UsersController', () => { expect(controller).toBeDefined(); }); + describe('GET /volunteers', () => { + it('should return all volunteers', async () => { + const users: (Omit, 'pantries'> & { + pantryIds: number[]; + })[] = [ + { + id: 1, + role: Role.VOLUNTEER, + pantryIds: [1], + }, + { + id: 2, + role: Role.VOLUNTEER, + pantryIds: [2], + }, + { + id: 3, + role: Role.ADMIN, + pantryIds: [3], + }, + ]; + + const volunteers = users.slice(0, 2); + + mockUserService.getVolunteersAndPantryAssignments.mockResolvedValue( + volunteers as (Omit & { pantryIds: number[] })[], + ); + + const result = await controller.getAllVolunteers(); + + expect(result).toEqual(volunteers); + expect(result.length).toBe(2); + expect(result.every((u) => u.role === Role.VOLUNTEER)).toBe(true); + expect( + mockUserService.getVolunteersAndPantryAssignments, + ).toHaveBeenCalled(); + }); + }); + describe('GET /:id', () => { it('should return a user by id', async () => { mockUserService.findOne.mockResolvedValue(mockUser1 as User); @@ -142,7 +189,7 @@ describe('UsersController', () => { firstName: 'Jane', lastName: 'Smith', phone: '9876543210', - role: Role.STANDARD_VOLUNTEER, + role: Role.VOLUNTEER, }; const error = new Error('Database error'); diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index db28dc0e..3cb9b77a 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -13,14 +13,14 @@ import { PantriesService } from '../pantries/pantries.service'; const mockUserRepository = mock>(); const mockPantriesService = mock(); -const mockUser = { +const mockUser: User = { id: 1, email: 'test@example.com', firstName: 'John', lastName: 'Doe', phone: '1234567890', - role: Role.STANDARD_VOLUNTEER, -} as User; + role: Role.VOLUNTEER, +}; describe('UsersService', () => { let service: UsersService; @@ -203,7 +203,7 @@ describe('UsersService', () => { describe('findUsersByRoles', () => { it('should return users by roles', async () => { - const roles = [Role.ADMIN, Role.LEAD_VOLUNTEER]; + const roles = [Role.ADMIN, Role.VOLUNTEER]; const users = [mockUser]; mockUserRepository.find.mockResolvedValue(users); diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 1f0984e3..65f90ae1 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -7,7 +7,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { User } from './user.entity'; -import { Role, VOLUNTEER_ROLES } from './types'; +import { Role } from './types'; import { validateId } from '../utils/validation.utils'; import { Pantry } from '../pantries/pantries.entity'; import { PantriesService } from '../pantries/pantries.service'; @@ -60,7 +60,7 @@ export class UsersService { if (!volunteer) throw new NotFoundException(`User ${volunteerId} not found`); - if (!VOLUNTEER_ROLES.includes(volunteer.role)) { + if (volunteer.role !== Role.VOLUNTEER) { throw new BadRequestException(`User ${volunteerId} is not a volunteer`); } return volunteer; @@ -106,7 +106,7 @@ export class UsersService { async getVolunteersAndPantryAssignments(): Promise< (Omit & { pantryIds: number[] })[] > { - const volunteers = await this.findUsersByRoles(VOLUNTEER_ROLES); + const volunteers = await this.findUsersByRoles([Role.VOLUNTEER]); return volunteers.map((v) => { const { pantries, ...volunteerWithoutPantries } = v; diff --git a/apps/frontend/src/components/forms/pantryApplicationForm.tsx b/apps/frontend/src/components/forms/pantryApplicationForm.tsx index 94abfe3a..df185caa 100644 --- a/apps/frontend/src/components/forms/pantryApplicationForm.tsx +++ b/apps/frontend/src/components/forms/pantryApplicationForm.tsx @@ -67,18 +67,22 @@ const activityOptions = [ const PantryApplicationForm: React.FC = () => { const [contactPhone, setContactPhone] = useState(''); - const [secondaryContactPhone, setSecondaryContactPhone] = useState(''); + const [secondaryContactPhone, setSecondaryContactPhone] = + useState(''); const [activities, setActivities] = useState([]); const allergenClientsExactOption: string = 'I have an exact number'; const [allergenClients, setAllergenClients] = useState(); const [restrictions, setRestrictions] = useState([]); - const [reserveFoodForAllergic, setReserveFoodForAllergic] = useState(); - const [differentMailingAddress, setDifferentMailingAddress] = useState(); + const [reserveFoodForAllergic, setReserveFoodForAllergic] = + useState(); + const [differentMailingAddress, setDifferentMailingAddress] = useState< + boolean | null + >(); const [otherEmailContact, setOtherEmailContact] = useState(false); const sectionTitleStyles = { - fontFamily: "inter", + fontFamily: 'inter', fontWeight: '600', fontSize: 'md', color: 'gray.dark', @@ -86,16 +90,16 @@ const PantryApplicationForm: React.FC = () => { }; const sectionSubtitleStyles = { - fontFamily: "inter", + fontFamily: 'inter', fontWeight: '400', color: 'gray.light', mb: '2.25em', fontSize: 'sm', - } + }; const fieldHeaderStyles = { color: 'neutral.800', - fontFamily: "inter", + fontFamily: 'inter', fontSize: 'sm', fontWeight: '600', }; @@ -103,96 +107,121 @@ const PantryApplicationForm: React.FC = () => { return ( - + Partner Pantry Application - Thank you for your interest in partnering with Securing Safe Food (SSF) to help - serve clients with food allergies and other adverse reactions to foods. + Thank you for your interest in partnering with Securing Safe Food + (SSF) to help serve clients with food allergies and other adverse + reactions to foods. -
- + Pantry Application Form - + - This application helps us understand your pantry’s capacity and interest in - distributing allergen-friendly food. We’ll ask about your pantry’s current - practices, storage capabilities, and communication preferences. + This application helps us understand your pantry’s capacity and + interest in distributing allergen-friendly food. We’ll ask about + your pantry’s current practices, storage capabilities, and + communication preferences. - Please answer as accurately as possible. If you have any questions or need help, - don’t hesitate to contact the SSF team. + Please answer as accurately as possible. If you have any questions + or need help, don’t hesitate to contact the SSF team. - - - Primary Contact Information - + + Primary Contact Information First Name - + - + Last Name - + - + Phone Number - + Email Address - + - + - Is there someone at your pantry who can regularly check and respond to emails from SSF as needed?{' '} - + Is there someone at your pantry who can regularly check and + respond to emails from SSF as needed?{' '} + - setOtherEmailContact(e.value === 'Other')} + onValueChange={(e: { value: string }) => + setOtherEmailContact(e.value === 'Other') + } > {['Yes', 'No', 'Other'].map((value) => ( - - - + + - + {value} @@ -201,127 +230,151 @@ const PantryApplicationForm: React.FC = () => { - - + - - Secondary Contact Information - + Secondary Contact Information - - First Name - - + First Name + - - Last Name - - + Last Name + - - Phone Number - + Phone Number - - Email Address - - + Email Address + - - + + Food Shipment Address Please list your address for food shipments. - + Address Line 1 - + - + - - Address Line 2 - - + Address Line 2 + City/Town - + - + State/Region/Province - + - + Zip/Post Code - + - + - - Country - - + Country + - Does this address differ from your pantry's mailing address for documents?{' '} - + Does this address differ from your pantry's mailing address for + documents? - setDifferentMailingAddress(e.value === 'Yes')} + onValueChange={(e: { value: string }) => + setDifferentMailingAddress(e.value === 'Yes') + } name="differentMailingAddress" > {['Yes', 'No'].map((value) => ( - - - + + - + {value} @@ -331,28 +384,22 @@ const PantryApplicationForm: React.FC = () => { - Would your pantry be able to accept food deliveries - during standard business hours Mon-Fri?{' '} - + Would your pantry be able to accept food deliveries during + standard business hours Mon-Fri?{' '} + - + {['Yes', 'No'].map((value) => ( - - + - + {value} @@ -364,10 +411,13 @@ const PantryApplicationForm: React.FC = () => { Please note any delivery window restrictions. -