diff --git a/apps/backend/src/app/app.module.ts b/apps/backend/src/app/app.module.ts index 920a82eb..f44575cb 100644 --- a/apps/backend/src/app/app.module.ts +++ b/apps/backend/src/app/app.module.ts @@ -7,11 +7,12 @@ import { AppService } from './app.service'; import { SiteModule } from '../site/site.module'; import { ApplicationsModule } from '../applications/applications.module'; import { DynamoDbService } from '../dynamodb'; +import { LambdaService } from '../lambda'; @Module({ imports: [SiteModule,UserModule, ApplicationsModule], controllers: [AppController], - providers: [AppService, DynamoDbService], + providers: [AppService, DynamoDbService, LambdaService], }) export class AppModule {} diff --git a/apps/backend/src/applications/applications.module.ts b/apps/backend/src/applications/applications.module.ts index 2e32c1bc..1c9d59c0 100644 --- a/apps/backend/src/applications/applications.module.ts +++ b/apps/backend/src/applications/applications.module.ts @@ -2,10 +2,13 @@ import { Module } from '@nestjs/common'; import { ApplicationsService } from './applications.service'; import { ApplicationsController } from './applications.controller'; import { DynamoDbService } from '../dynamodb'; +import { LambdaService } from '../lambda'; +import { UserService } from '../user/user.service'; +import { SiteService } from '../site/site.service'; @Module({ imports: [], - providers: [ApplicationsService, DynamoDbService], + providers: [ApplicationsService, DynamoDbService, LambdaService, UserService, SiteService], controllers: [ApplicationsController], exports: [ApplicationsService], }) diff --git a/apps/backend/src/applications/applications.service.ts b/apps/backend/src/applications/applications.service.ts index c27b2fa9..1531ef9b 100644 --- a/apps/backend/src/applications/applications.service.ts +++ b/apps/backend/src/applications/applications.service.ts @@ -3,11 +3,15 @@ import { ApplicationInputModel, ApplicationsModel } from './applications.model'; import { DynamoDbService } from '../dynamodb'; import { ApplicationStatus } from './applications.model'; import { NewApplicationInput } from '../dtos/newApplicationsDTO'; +import { LambdaService } from '../lambda'; +import { UserService } from '../user/user.service'; +import { SiteService } from '../site/site.service'; @Injectable() export class ApplicationsService { private readonly tableName = 'gibostonApplications'; - constructor(private readonly dynamoDbService: DynamoDbService) {} + constructor(private readonly dynamoDbService: DynamoDbService, private readonly lambdaService:LambdaService, + private readonly userService: UserService, private readonly siteService: SiteService) {} /** * Gets all applications. @@ -75,6 +79,26 @@ export class ApplicationsService { console.log("Received application data:", applicationData); try { const result = await this.dynamoDbService.postItem(this.tableName, applicationModel); + + if (result.$metadata.httpStatusCode !== 200) { + throw new Error('Error posting application'); + } + const user = await this.userService.getUser(applicationData.userId); + const site = await this.siteService.getSite(applicationData.siteId); + const name = user.firstName; + const email = user.email; + + const siteName = site.siteName; + const timeFrame = "30 days" + const emailData = {"firstName":name, "userEmail":email, "siteName":siteName, "timeFrame":timeFrame}; + + + + const lambdaResult = await this.lambdaService.invokeLambda('giSendApplicationConfirmation', emailData ); + console.log("Lambda result: ", lambdaResult); + + + return {...result, newApplicationId: newId.toString()}; } catch (e) { throw new Error("Unable to post new application: " + e); diff --git a/apps/backend/src/dtos/newApplicationsDTO.ts b/apps/backend/src/dtos/newApplicationsDTO.ts index 527b3b5b..8f3ef15e 100644 --- a/apps/backend/src/dtos/newApplicationsDTO.ts +++ b/apps/backend/src/dtos/newApplicationsDTO.ts @@ -1,15 +1,60 @@ -export type NewApplicationInput = { - userId: number; - siteId: number; - names: string[]; // Array with empty string by default if not provided - status: ApplicationStatus; // Defaults to "PENDING" - dateApplied: string; // Defaults to an ISO string if not provided - isFirstApplication: boolean; - }; - - export enum ApplicationStatus { - APPROVED = 'Approved', - PENDING = 'Pending', - DENIED = 'Denied', - } - +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsArray, IsString, IsBoolean, IsEnum, IsOptional } from 'class-validator'; + +export enum ApplicationStatus { + APPROVED = 'Approved', + PENDING = 'Pending', + DENIED = 'Denied', +} + +export class NewApplicationInput { + @ApiProperty({ + description: 'User ID of the applicant', + example: 123, + }) + @IsNumber() + userId: number; + + @ApiProperty({ + description: 'Site ID associated with the application', + example: 456, + }) + @IsNumber() + siteId: number; + + @ApiProperty({ + description: 'Array of names associated with the application (defaults to an empty array if not provided)', + example: ['John Doe', 'Jane Doe'], + required: false, + }) + @IsArray() + @IsString({ each: true }) + @IsOptional() + names: string[] = []; // Default to an empty array if not provided + + @ApiProperty({ + description: 'Status of the application', + enum: ApplicationStatus, + example: ApplicationStatus.PENDING, + required: false, + }) + @IsEnum(ApplicationStatus) + @IsOptional() + status: ApplicationStatus = ApplicationStatus.PENDING; // Default to "PENDING" + + @ApiProperty({ + description: 'Date the application was submitted (defaults to an ISO string if not provided)', + example: '2025-02-06T10:00:00Z', + required: false, + }) + @IsString() + @IsOptional() + dateApplied: string = new Date().toISOString(); // Default to ISO string + + @ApiProperty({ + description: 'Indicates if this is the user’s first application', + example: true, + }) + @IsBoolean() + isFirstApplication: boolean; +} diff --git a/apps/backend/src/lambda.ts b/apps/backend/src/lambda.ts new file mode 100644 index 00000000..6c90ce5e --- /dev/null +++ b/apps/backend/src/lambda.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { LambdaClient, InvokeCommand } from '@aws-sdk/client-lambda'; + +@Injectable() +export class LambdaService { + private readonly lambdaClient: LambdaClient; + + constructor() { + this.lambdaClient = new LambdaClient({ + region: process.env.AWS_REGION, + }); + } + + public async invokeLambda(functionName: string, payload: Record) { + try { + const command = new InvokeCommand({ + FunctionName: functionName, + Payload: Buffer.from(JSON.stringify(payload)), + }); + + const response = await this.lambdaClient.send(command); + + if (response.Payload) { + return JSON.parse(Buffer.from(response.Payload).toString()); + } + + throw new Error('No response payload from Lambda function'); + } catch (error) { + console.error('Error invoking Lambda:', error); + throw error; + } + } +} diff --git a/apps/frontend/src/components/volunteer/signup/SignUpPage.tsx b/apps/frontend/src/components/volunteer/signup/SignUpPage.tsx index 50d85d35..0e325da8 100644 --- a/apps/frontend/src/components/volunteer/signup/SignUpPage.tsx +++ b/apps/frontend/src/components/volunteer/signup/SignUpPage.tsx @@ -6,6 +6,7 @@ import { VStack, Button, IconButton, + Textarea, } from '@chakra-ui/react'; import { Checkbox } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; @@ -26,20 +27,21 @@ interface CheckboxField { interface InputFieldGroup { fields: InputField[]; - type?: 'single' | 'double'; // 'single' for single column, 'double' for double column row + type?: 'single' | 'double'; height: string; width: string; } const personalInfoCheckboxesMap: CheckboxField[] = [ - { - label: 'Signing up as a group representative?', - }, + { label: 'Signing up as a group representative?' }, ]; const personalInfoInputFieldsMap: InputFieldGroup[] = [ { - fields: [{ label: 'First Name', width: '250px'}, { label: 'Last Name', width: '350px' }], + fields: [ + { label: 'First Name', width: '250px' }, + { label: 'Last Name', width: '350px' }, + ], type: 'double', height: '40px', width: '810px', @@ -65,127 +67,142 @@ const personalInfoInputFieldsMap: InputFieldGroup[] = [ ]; const termsAndConditionsCheckboxesMap: CheckboxField[] = [ - { - label: 'I have reviewed the General Safety Guidelines', - }, - { - label: 'I have read and agree to the Terms of Use and Privacy Policy', - }, - { - label: 'I have read and agree to the Release of Liability', - }, + { label: 'I have reviewed the General Safety Guidelines' }, + { label: 'I have read and agree to the Terms of Use and Privacy Policy' }, + { label: 'I have read and agree to the Release of Liability' }, ]; function PersonalInfo() { + const [isGroupRepresentative, setIsGroupRepresentative] = useState(false); + return ( - + {personalInfoCheckboxesMap.map((field, i) => ( - + {field.label} setIsGroupRepresentative(e.target.checked)} sx={{ - color: '#808080', // Grey color for the checkbox when not checked - '&.Mui-checked': { - color: '#808080', // Grey color for the checkbox when checked - }, - '& .MuiSvgIcon-root': { - fontSize: 23, - }, + color: '#808080', + '&.Mui-checked': { color: '#808080' }, + '& .MuiSvgIcon-root': { fontSize: 23 }, padding: '2px', marginLeft: '20px', }} /> ))} - {personalInfoInputFieldsMap.map((group, i) => ( - - {group.type === 'double' ? ( - - {group.fields.map((field, j) => ( - - - {field.label} - - - - ))} - - ) : ( - + + {/* Name Fields */} + + + {personalInfoInputFieldsMap[0].fields.map((field, j) => ( + + {field.label} + + + + ))} + + + + {/* Lower Section */} + + {/* Left Column - Email/Phone/Birth Year */} + + {personalInfoInputFieldsMap.slice(1).map((group, i) => ( + + {group.fields[0].label} - )} + ))} - ))} + + {/* Right Column - Group Members */} + {isGroupRepresentative && ( + + + Group Members + +