diff --git a/apps/backend/src/auth/auth.module.ts b/apps/backend/src/auth/auth.module.ts index 09f5965c..eac8a5b6 100644 --- a/apps/backend/src/auth/auth.module.ts +++ b/apps/backend/src/auth/auth.module.ts @@ -1,19 +1,14 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; import { PassportModule } from '@nestjs/passport'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; -import { UsersService } from '../users/users.service'; -import { User } from '../users/user.entity'; import { JwtStrategy } from './jwt.strategy'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [ - TypeOrmModule.forFeature([User]), - PassportModule.register({ defaultStrategy: 'jwt' }), - ], + imports: [UsersModule, PassportModule.register({ defaultStrategy: 'jwt' })], controllers: [AuthController], - providers: [AuthService, UsersService, JwtStrategy], + providers: [AuthService, JwtStrategy], }) export class AuthModule {} diff --git a/apps/backend/src/config/typeorm.ts b/apps/backend/src/config/typeorm.ts index 1facc961..1503d8ff 100644 --- a/apps/backend/src/config/typeorm.ts +++ b/apps/backend/src/config/typeorm.ts @@ -16,6 +16,7 @@ import { UpdateFoodRequests1744051370129 } from '../migrations/1744051370129-upd import { UpdateRequestTable1741571847063 } from '../migrations/1741571847063-updateRequestTable'; import { RemoveOrderIdFromRequests1744133526650 } from '../migrations/1744133526650-removeOrderIdFromRequests.ts'; import { AddOrders1739496585940 } from '../migrations/1739496585940-addOrders'; +import { AddVolunteerPantryUniqueConstraint1760033134668 } from '../migrations/1760033134668-AddVolunteerPantryUniqueConstraint'; const config = { type: 'postgres', @@ -45,6 +46,7 @@ const config = { UpdateRequestTable1741571847063, UpdateFoodRequests1744051370129, RemoveOrderIdFromRequests1744133526650, + AddVolunteerPantryUniqueConstraint1760033134668, ], }; diff --git a/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts new file mode 100644 index 00000000..90e94be5 --- /dev/null +++ b/apps/backend/src/migrations/1760033134668-AddVolunteerPantryUniqueConstraint.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddVolunteerPantryUniqueConstraint1760033134668 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE volunteer_assignments DROP COLUMN assignment_id; + + ALTER TABLE volunteer_assignments + ADD PRIMARY KEY (volunteer_id, pantry_id); + + ALTER TABLE volunteer_assignments DROP CONSTRAINT IF EXISTS fk_volunteer_id; + + ALTER TABLE volunteer_assignments DROP CONSTRAINT IF EXISTS fk_pantry_id; + + ALTER TABLE volunteer_assignments + ADD CONSTRAINT fk_volunteer_id FOREIGN KEY (volunteer_id) REFERENCES users(user_id) ON DELETE CASCADE, + ADD CONSTRAINT fk_pantry_id FOREIGN KEY (pantry_id) REFERENCES pantries(pantry_id) ON DELETE CASCADE; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE volunteer_assignments DROP CONSTRAINT fk_volunteer_id; + + ALTER TABLE volunteer_assignments DROP CONSTRAINT fk_pantry_id; + + ALTER TABLE volunteer_assignments DROP CONSTRAINT volunteer_assignments_pkey; + + ALTER TABLE volunteer_assignments ADD COLUMN assignment_id SERIAL PRIMARY KEY; + `); + } +} diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index 686a8529..0d521099 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -5,6 +5,7 @@ import { Param, ParseIntPipe, Put, + Patch, BadRequestException, Body, //UseGuards, @@ -14,7 +15,6 @@ import { UsersService } from './users.service'; //import { AuthGuard } from '@nestjs/passport'; import { User } from './user.entity'; import { Role } from './types'; -import { VOLUNTEER_ROLES } from './types'; //import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; @Controller('users') @@ -24,7 +24,7 @@ export class UsersController { @Get('/volunteers') async getAllVolunteers(): Promise { - return this.usersService.findUsersByRoles(VOLUNTEER_ROLES); + return this.usersService.getVolunteersAndPantryAssignments(); } // @UseGuards(AuthGuard('jwt')) @@ -48,4 +48,17 @@ export class UsersController { } return this.usersService.update(id, { role: role as Role }); } + + @Get('/:id/pantries') + async getVolunteerPantries(@Param('id', ParseIntPipe) id: number) { + return this.usersService.getVolunteerPantries(id); + } + + @Patch(':id/pantries') + async assignPantry( + @Param('id', ParseIntPipe) id: number, + @Body('pantryIds') pantryIds: number[], + ) { + return this.usersService.assignPantriesToVolunteer(id, pantryIds); + } } diff --git a/apps/backend/src/users/users.module.ts b/apps/backend/src/users/users.module.ts index 2f78bb05..a5c98467 100644 --- a/apps/backend/src/users/users.module.ts +++ b/apps/backend/src/users/users.module.ts @@ -6,10 +6,12 @@ import { User } from './user.entity'; import { JwtStrategy } from '../auth/jwt.strategy'; import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; import { AuthService } from '../auth/auth.service'; +import { Assignments } from '../volunteerAssignments/volunteerAssignments.entity'; +import { Pantry } from '../pantries/pantries.entity'; @Module({ - imports: [TypeOrmModule.forFeature([User])], - exports: [UsersService], + imports: [TypeOrmModule.forFeature([User, Assignments, Pantry])], + exports: [UsersService, TypeOrmModule], controllers: [UsersController], providers: [UsersService, AuthService, JwtStrategy, CurrentUserInterceptor], }) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index f75037c8..ae290491 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -1,14 +1,29 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { User } from './user.entity'; -import { Role } from './types'; +import { Role, VOLUNTEER_ROLES } from './types'; import { validateId } from '../utils/validation.utils'; +import { Assignments } from '../volunteerAssignments/volunteerAssignments.entity'; +import { Pantry } from '../pantries/pantries.entity'; @Injectable() export class UsersService { - constructor(@InjectRepository(User) private repo: Repository) {} + constructor( + @InjectRepository(User) + private repo: Repository, + + @InjectRepository(Assignments) + private assignmentsRepo: Repository, + + @InjectRepository(Pantry) + private pantryRepo: Repository, + ) {} async create( email: string, @@ -72,4 +87,52 @@ export class UsersService { async findUsersByRoles(roles: Role[]): Promise { return this.repo.find({ where: { role: In(roles) } }); } + + async getVolunteersAndPantryAssignments() { + const volunteers = await this.findUsersByRoles(VOLUNTEER_ROLES); + + const assignments = await this.assignmentsRepo.find({ + relations: ['pantry', 'volunteer'], + }); + + return volunteers.map((v) => { + const assigned = assignments + .filter((a) => a.volunteer.id == v.id) + .map((a) => a.pantry.pantryId); + return { ...v, pantryIds: assigned }; + }); + } + + async getVolunteerPantries(volunteerId: number) { + validateId(volunteerId, 'Volunteer'); + const assignments = await this.assignmentsRepo.find({ + where: { volunteer: { id: volunteerId } }, + relations: ['pantry'], + }); + + return assignments.map((a) => a.pantry); + } + + async assignPantriesToVolunteer(volunteerId: number, pantryIds: number[]) { + validateId(volunteerId, 'Volunteer'); + for (const pantryId of pantryIds) { + validateId(pantryId, 'Pantry'); + } + + const volunteer = await this.repo.findOne({ where: { id: volunteerId } }); + if (!volunteer) + throw new NotFoundException(`Volunteer ${volunteerId} not found`); + + const pantries = await this.pantryRepo.findBy({ pantryId: In(pantryIds) }); + + if (pantries.length !== pantryIds.length) { + throw new BadRequestException('One or more pantries not found'); + } + + const assignments = pantries.map((pantry) => + this.assignmentsRepo.create({ volunteer, pantry }), + ); + + return this.assignmentsRepo.save(assignments); + } } diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts index 438c79aa..9d7bca73 100644 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts +++ b/apps/backend/src/volunteerAssignments/volunteerAssignments.entity.ts @@ -1,30 +1,23 @@ -import { - Entity, - PrimaryGeneratedColumn, - OneToOne, - JoinColumn, - ManyToOne, - Column, -} from 'typeorm'; +import { Entity, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; import { User } from '../users/user.entity'; import { Pantry } from '../pantries/pantries.entity'; @Entity('volunteer_assignments') export class Assignments { - @PrimaryGeneratedColumn({ name: 'assignment_id' }) - assignmentId: number; - - @Column({ name: 'volunteer_id' }) + @PrimaryColumn({ name: 'volunteer_id' }) volunteerId: number; - @ManyToOne(() => User, { nullable: false }) + @PrimaryColumn({ name: 'pantry_id' }) + pantryId: number; + + @ManyToOne(() => User, { nullable: false, onDelete: 'CASCADE' }) @JoinColumn({ name: 'volunteer_id', referencedColumnName: 'id', }) volunteer: User; - @OneToOne(() => Pantry, { nullable: true }) + @ManyToOne(() => Pantry, { nullable: false, onDelete: 'CASCADE' }) @JoinColumn({ name: 'pantry_id', referencedColumnName: 'pantryId', diff --git a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts b/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts index d6ef19a1..d20fff30 100644 --- a/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts +++ b/apps/backend/src/volunteerAssignments/volunteerAssignments.service.ts @@ -11,12 +11,11 @@ export class AssignmentsService { private usersService: UsersService, ) {} - // Gets the assignment id, volunteer details and the corresponding pantry + // Gets the volunteer details and the corresponding pantry async getAssignments() { const results = await this.repo.find({ relations: ['volunteer', 'pantry'], select: { - assignmentId: true, volunteer: { id: true, firstName: true, diff --git a/apps/frontend/src/types/types.ts b/apps/frontend/src/types/types.ts index 39c93dc7..5d7e66e5 100644 --- a/apps/frontend/src/types/types.ts +++ b/apps/frontend/src/types/types.ts @@ -139,7 +139,6 @@ export enum VolunteerType { } export interface VolunteerPantryAssignment { - assignmentId: number; volunteer: { id: number; firstName: string;