diff --git a/backend/src/grant/grant.controller.ts b/backend/src/grant/grant.controller.ts index 287cd27..0a590e8 100644 --- a/backend/src/grant/grant.controller.ts +++ b/backend/src/grant/grant.controller.ts @@ -2,58 +2,122 @@ import { Controller, Get, Param, Put, Body, Patch, Post, Delete, ValidationPipe, import { GrantService } from './grant.service'; import { Grant } from '../../../middle-layer/types/Grant'; import { VerifyUserGuard } from '../guards/auth.guard'; -import { ApiBearerAuth } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiOperation, ApiResponse, ApiParam, ApiBody, ApiTags } from '@nestjs/swagger'; +import { InactivateGrantBody, AddGrantBody, UpdateGrantBody, GrantResponseDto } from './types/grant.types'; +@ApiTags('grant') @Controller('grant') export class GrantController { + private readonly logger = new Logger(GrantController.name); + constructor(private readonly grantService: GrantService) { } @Get() @UseGuards(VerifyUserGuard) @ApiBearerAuth() - async getAllGrants() { - return await this.grantService.getAllGrants(); + @ApiOperation({ summary: 'Retrieve all grants', description: 'Returns a list of all grants in the database. Automatically inactivates expired grants.' }) + @ApiResponse({ status: 200, description: 'Successfully retrieved all grants', type: [GrantResponseDto] }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) + async getAllGrants(): Promise { + this.logger.log('GET /grant - Retrieving all grants'); + const grants = await this.grantService.getAllGrants(); + this.logger.log(`GET /grant - Successfully retrieved ${grants.length} grants`); + return grants; } - - @Put('inactivate') @UseGuards(VerifyUserGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Inactivate grants', description: 'Marks one or more grants as inactive by their grant IDs' }) + @ApiBody({ type: InactivateGrantBody, description: 'Array of grant IDs to inactivate' }) + @ApiResponse({ status: 200, description: 'Successfully inactivated grants', type: [GrantResponseDto] }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async inactivate( - @Body('grantIds') grantIds: number[] + @Body() body: InactivateGrantBody ): Promise { + this.logger.log(`PUT /grant/inactivate - Inactivating ${body.grantIds.length} grant(s)`); let grants: Grant[] = []; - for(const id of grantIds){ - Logger.log(`Inactivating grant with ID: ${id}`); + for(const id of body.grantIds){ + this.logger.debug(`Inactivating grant with ID: ${id}`); let newGrant = await this.grantService.makeGrantsInactive(id) grants.push(newGrant); } + this.logger.log(`PUT /grant/inactivate - Successfully inactivated ${grants.length} grant(s)`); return grants; } @Post('new-grant') @UseGuards(VerifyUserGuard) + @ApiBearerAuth() + @ApiOperation({ summary: 'Create a new grant', description: 'Creates a new grant in the database with a generated grant ID' }) + @ApiBody({ type: AddGrantBody, description: 'Grant data to create' }) + @ApiResponse({ status: 201, description: 'Successfully created grant', type: Number, example: 1234567890 }) + @ApiResponse({ status: 400, description: 'Bad Request - Invalid grant data', example: '{Error encountered}' }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) async addGrant( @Body(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true })) - grant: Grant - ) { - return await this.grantService.addGrant(grant); + grant: AddGrantBody + ): Promise { + this.logger.log(`POST /grant/new-grant - Creating new grant for organization: ${grant.organization}`); + const grantId = await this.grantService.addGrant(grant as Grant); + this.logger.log(`POST /grant/new-grant - Successfully created grant with ID: ${grantId}`); + return grantId; } @Put('save') @UseGuards(VerifyUserGuard) - async saveGrant(@Body() grantData: Grant) { - return await this.grantService.updateGrant(grantData) + @ApiBearerAuth() + @ApiOperation({ summary: 'Update an existing grant', description: 'Updates an existing grant in the database with new grant data' }) + @ApiBody({ type: UpdateGrantBody, description: 'Updated grant data including grantId' }) + @ApiResponse({ status: 200, description: 'Successfully updated grant', type: String, example: '{"Attributes": {...}}' }) + @ApiResponse({ status: 400, description: 'Bad Request - Invalid grant data', example: '{Error encountered}' }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) + async saveGrant(@Body() grantData: UpdateGrantBody): Promise { + this.logger.log(`PUT /grant/save - Updating grant with ID: ${grantData.grantId}`); + const result = await this.grantService.updateGrant(grantData as Grant); + this.logger.log(`PUT /grant/save - Successfully updated grant ${grantData.grantId}`); + return result; } @Delete(':grantId') @UseGuards(VerifyUserGuard) - async deleteGrant(@Param('grantId') grantId: number) { - return await this.grantService.deleteGrantById(grantId); + @ApiBearerAuth() + @ApiOperation({ summary: 'Delete a grant', description: 'Deletes a grant from the database by its grant ID' }) + @ApiParam({ name: 'grantId', type: Number, description: 'The ID of the grant to delete' }) + @ApiResponse({ status: 200, description: 'Successfully deleted grant', type: String, example: 'Grant 1234567890 deleted successfully' }) + @ApiResponse({ status: 400, description: 'Bad Request - Grant does not exist', example: '{Error encountered}' }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) + async deleteGrant(@Param('grantId') grantId: number): Promise { + this.logger.log(`DELETE /grant/${grantId} - Deleting grant`); + const result = await this.grantService.deleteGrantById(grantId); + this.logger.log(`DELETE /grant/${grantId} - Successfully deleted grant`); + return result; } + @Get(':id') @UseGuards(VerifyUserGuard) - async getGrantById(@Param('id') GrantId: string) { - return await this.grantService.getGrantById(parseInt(GrantId, 10)); + @ApiBearerAuth() + @ApiOperation({ summary: 'Get a grant by ID', description: 'Retrieves a single grant from the database by its grant ID' }) + @ApiParam({ name: 'id', type: String, description: 'The ID of the grant to retrieve' }) + @ApiResponse({ status: 200, description: 'Successfully retrieved grant', type: GrantResponseDto }) + @ApiResponse({ status: 404, description: 'Grant not found', example: '{Error encountered}' }) + @ApiResponse({ status: 401, description: 'Unauthorized - Invalid or missing authentication token' }) + @ApiResponse({ status: 403, description: 'Forbidden - User does not have access to this resource' }) + @ApiResponse({ status: 500, description: 'Internal Server Error', example: 'Internal Server Error' }) + async getGrantById(@Param('id') GrantId: string): Promise { + this.logger.log(`GET /grant/${GrantId} - Retrieving grant by ID`); + const grant = await this.grantService.getGrantById(parseInt(GrantId, 10)); + this.logger.log(`GET /grant/${GrantId} - Successfully retrieved grant`); + return grant; } } \ No newline at end of file diff --git a/backend/src/grant/grant.service.ts b/backend/src/grant/grant.service.ts index 8671c03..9903fb8 100644 --- a/backend/src/grant/grant.service.ts +++ b/backend/src/grant/grant.service.ts @@ -12,20 +12,23 @@ export class GrantService { constructor(private readonly notificationService: NotificationService) {} - // function to retrieve all grants in our database + // Retrieves all grants from the database and automatically inactivates expired grants async getAllGrants(): Promise { - console.log("GETTING ALL GRANTS"); - // loads in the environment variable for the table now + this.logger.log('Starting to retrieve all grants from database'); const params = { TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', }; try { + this.logger.debug(`Scanning DynamoDB table: ${params.TableName}`); const data = await this.dynamoDb.scan(params).promise(); const grants = (data.Items as Grant[]) || []; + this.logger.log(`Retrieved ${grants.length} grants from database`); + const inactiveGrantIds: number[] = []; const now = new Date(); + this.logger.debug('Checking for expired active grants'); for (const grant of grants) { if (grant.status === "Active") { const startDate = new Date(grant.grant_start_date); @@ -37,6 +40,7 @@ export class GrantService { ); if (now >= endDate) { + this.logger.warn(`Grant ${grant.grantId} has expired and will be marked inactive`); inactiveGrantIds.push(grant.grantId); let newGrant = this.makeGrantsInactive(grant.grantId) grants.filter(g => g.grantId !== grant.grantId); @@ -45,16 +49,22 @@ export class GrantService { } } } - return grants; + + if (inactiveGrantIds.length > 0) { + this.logger.log(`Automatically inactivated ${inactiveGrantIds.length} expired grants`); + } + + this.logger.log(`Successfully retrieved ${grants.length} grants`); + return grants; } catch (error) { - console.log(error) + this.logger.error('Failed to retrieve grants from database', error instanceof Error ? error.stack : undefined); throw new Error('Could not retrieve grants.'); } } - // function to retrieve a grant by its ID + // Retrieves a single grant from the database by its unique grant ID async getGrantById(grantId: number): Promise { - + this.logger.log(`Retrieving grant with ID: ${grantId}`); const params = { TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', Key: { @@ -63,24 +73,31 @@ export class GrantService { }; try { + this.logger.debug(`Querying DynamoDB for grant ID: ${grantId}`); const data = await this.dynamoDb.get(params).promise(); if (!data.Item) { + this.logger.warn(`Grant with ID ${grantId} not found in database`); throw new NotFoundException('No grant with id ' + grantId + ' found.'); } + this.logger.log(`Successfully retrieved grant ${grantId} from database`); return data.Item as Grant; } catch (error) { - if (error instanceof NotFoundException) throw error; + if (error instanceof NotFoundException) { + this.logger.warn(`Grant ${grantId} not found: ${error.message}`); + throw error; + } - console.log(error) + this.logger.error(`Failed to retrieve grant ${grantId}`, error instanceof Error ? error.stack : undefined); throw new Error('Failed to retrieve grant.'); } } - // Method to make grants inactive -async makeGrantsInactive(grantId: number): Promise { - let updatedGrant: Grant = {} as Grant; + // Marks a grant as inactive by updating its status in the database + async makeGrantsInactive(grantId: number): Promise { + this.logger.log(`Marking grant ${grantId} as inactive`); + let updatedGrant: Grant = {} as Grant; const params = { TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || "TABLE_FAILURE", @@ -96,37 +113,34 @@ async makeGrantsInactive(grantId: number): Promise { }; try { + this.logger.debug(`Updating grant ${grantId} status to inactive in DynamoDB`); const res = await this.dynamoDb.update(params).promise(); if (res.Attributes?.status === Status.Inactive) { - console.log(`Grant ${grantId} successfully marked as inactive.`); - + this.logger.log(`Grant ${grantId} successfully marked as inactive`); const currentGrant = res.Attributes as Grant; - console.log(currentGrant); - updatedGrant = currentGrant + updatedGrant = currentGrant; } else { - console.log(`Grant ${grantId} update failed or no change in status.`); + this.logger.warn(`Grant ${grantId} update failed or no change in status`); } } catch (err) { - console.log(err); + this.logger.error(`Failed to update grant ${grantId} status to inactive`, err instanceof Error ? err.stack : undefined); throw new Error(`Failed to update Grant ${grantId} status.`); } - return updatedGrant; -} + return updatedGrant; + } - /** - * Will push or overwrite new grant data to database - * @param grantData - */ + // Updates an existing grant in the database with new grant data async updateGrant(grantData: Grant): Promise { + this.logger.log(`Updating grant with ID: ${grantData.grantId}`); const updateKeys = Object.keys(grantData).filter( key => key != 'grantId' ); - this.logger.warn('Update keys: ' + JSON.stringify(updateKeys)); + this.logger.debug(`Updating ${updateKeys.length} fields for grant ${grantData.grantId}: ${updateKeys.join(', ')}`); const UpdateExpression = "SET " + updateKeys.map((key) => `#${key} = :${key}`).join(", "); const ExpressionAttributeNames = updateKeys.reduce((acc, key) => @@ -144,67 +158,70 @@ async makeGrantsInactive(grantId: number): Promise { }; try { + this.logger.debug(`Executing DynamoDB update for grant ${grantData.grantId}`); const result = await this.dynamoDb.update(params).promise(); - this.logger.warn('✅ Update successful!'); + this.logger.log(`Successfully updated grant ${grantData.grantId} in database`); //await this.updateGrantNotifications(grantData); return JSON.stringify(result); } catch(err: unknown) { - this.logger.error('=== DYNAMODB ERROR ==='); - this.logger.error('Unknown error type: ' + JSON.stringify(err)); + this.logger.error(`Failed to update grant ${grantData.grantId} in DynamoDB`, err instanceof Error ? err.stack : undefined); + this.logger.error(`Error details: ${JSON.stringify(err)}`); throw new Error(`Failed to update Grant ${grantData.grantId}`); } } - // Add a new grant using the Grant interface from middleware. - async addGrant(grant: Grant): Promise { - // Generate a unique grant ID (using Date.now() for simplicity, needs proper UUID) - const newGrantId = Date.now(); + // Creates a new grant in the database and generates a unique grant ID + async addGrant(grant: Grant): Promise { + this.logger.log(`Creating new grant for organization: ${grant.organization}`); + // Generate a unique grant ID (using Date.now() for simplicity, needs proper UUID) + const newGrantId = Date.now(); + this.logger.debug(`Generated grant ID: ${newGrantId}`); - const params = { - TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', - Item: { - grantId: newGrantId, - organization: grant.organization, - does_bcan_qualify: grant.does_bcan_qualify, - status: grant.status, - amount: grant.amount, - grant_start_date: grant.grant_start_date, - application_deadline: grant.application_deadline, - report_deadlines: grant.report_deadlines, - description: grant.description, - timeline: grant.timeline, - estimated_completion_time: grant.estimated_completion_time, - grantmaker_poc: grant.grantmaker_poc, - bcan_poc: grant.bcan_poc, - attachments: grant.attachments, - isRestricted: grant.isRestricted, + const params = { + TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || 'TABLE_FAILURE', + Item: { + grantId: newGrantId, + organization: grant.organization, + does_bcan_qualify: grant.does_bcan_qualify, + status: grant.status, + amount: grant.amount, + grant_start_date: grant.grant_start_date, + application_deadline: grant.application_deadline, + report_deadlines: grant.report_deadlines, + description: grant.description, + timeline: grant.timeline, + estimated_completion_time: grant.estimated_completion_time, + grantmaker_poc: grant.grantmaker_poc, + bcan_poc: grant.bcan_poc, + attachments: grant.attachments, + isRestricted: grant.isRestricted, + } + }; + + try { + this.logger.debug(`Inserting grant ${newGrantId} into DynamoDB`); + await this.dynamoDb.put(params).promise(); + this.logger.log(`Successfully created grant ${newGrantId} for organization: ${grant.organization}`); + + const userId = grant.bcan_poc.POC_email; + this.logger.debug(`Preparing to create notifications for user: ${userId}`); + + //await this.createGrantNotifications({ ...grant, grantId: newGrantId }, userId); + + this.logger.log(`Successfully created grant ${newGrantId} with all associated data`); + } catch (error: any) { + this.logger.error(`Failed to create new grant for organization: ${grant.organization}`); + this.logger.error(`Error details: ${error.message}`); + this.logger.error(`Stack trace: ${error.stack}`); + throw new Error(`Failed to upload new grant from ${grant.organization}`); } - }; - try { - await this.dynamoDb.put(params).promise(); - this.logger.log(`Uploaded grant from ${grant.organization}`); - - const userId = grant.bcan_poc.POC_email; - this.logger.log(`Creating notifications for user: ${userId}`); - - //await this.createGrantNotifications({ ...grant, grantId: newGrantId }, userId); - - this.logger.log(`Successfully created notifications for grant ${newGrantId}`); - } catch (error: any) { - this.logger.error(`Failed to upload new grant from ${grant.organization}`); - this.logger.error(`Error details: ${error.message}`); - this.logger.error(`Stack trace: ${error.stack}`); - throw new Error(`Failed to upload new grant from ${grant.organization}`); + return newGrantId; } - return newGrantId; - } - - /* Deletes a grant from database based on its grant ID number - * @param grantId - */ + // Deletes a grant from the database by its grant ID async deleteGrantById(grantId: number): Promise { + this.logger.log(`Deleting grant with ID: ${grantId}`); const params = { TableName: process.env.DYNAMODB_GRANT_TABLE_NAME || "TABLE_FAILURE", Key: { grantId: Number(grantId) }, @@ -212,22 +229,21 @@ async makeGrantsInactive(grantId: number): Promise { }; try { + this.logger.debug(`Executing DynamoDB delete for grant ${grantId}`); await this.dynamoDb.delete(params).promise(); - this.logger.log(`Grant ${grantId} deleted successfully`); + this.logger.log(`Successfully deleted grant ${grantId} from database`); return `Grant ${grantId} deleted successfully`; } catch (error: any) { if (error.code === "ConditionalCheckFailedException") { + this.logger.warn(`Grant ${grantId} does not exist in database`); throw new Error(`Grant ${grantId} does not exist`); } - this.logger.error(`Failed to delete Grant ${grantId}`, error.stack); + this.logger.error(`Failed to delete grant ${grantId}`, error.stack); throw new Error(`Failed to delete Grant ${grantId}`); } } - /* - Helper method that takes in a deadline in ISO format and returns an array of ISO strings representing the notification times - for 14 days, 7 days, and 3 days before the deadline. - */ + // Calculates notification times for a deadline (14, 7, and 3 days before) private getNotificationTimes(deadlineISO: string): string[] { const deadline = new Date(deadlineISO); const daysBefore = [14, 7, 3]; @@ -238,18 +254,23 @@ async makeGrantsInactive(grantId: number): Promise { }); } - /** - * Helper method that creates notifications for a grant's application and report deadlines - * @param grant represents the grant of which we want to create a notification for - * @param userId represents the user to whom we want to send the notification - */ + // Creates notifications for a grant's application and report deadlines private async createGrantNotifications(grant: Grant, userId: string) { const { grantId, organization, application_deadline, report_deadlines } = grant; - + this.logger.log( + `Creating notifications for grant ${grantId} (${organization}) for user ${userId}`, + ); + // Application deadline notifications if (application_deadline) { + this.logger.debug( + `Creating application deadline notifications for grant ${grantId} with deadline ${application_deadline}`, + ); const alertTimes = this.getNotificationTimes(application_deadline); for (const alertTime of alertTimes) { + this.logger.debug( + `Creating application notification for grant ${grantId} at alertTime ${alertTime}`, + ); const message = `Application due in ${this.daysUntil(alertTime, application_deadline)} days for ${organization}`; const notification: Notification = { notificationId: `${grantId}-app`, @@ -260,13 +281,23 @@ async makeGrantsInactive(grantId: number): Promise { }; await this.notificationService.createNotification(notification); } + } else { + this.logger.debug( + `No application_deadline found for grant ${grantId}; skipping application notifications`, + ); } - + // Report deadlines notifications if (report_deadlines && Array.isArray(report_deadlines)) { + this.logger.debug( + `Creating report deadline notifications for grant ${grantId} with ${report_deadlines.length} report_deadlines`, + ); for (const reportDeadline of report_deadlines) { const alertTimes = this.getNotificationTimes(reportDeadline); for (const alertTime of alertTimes) { + this.logger.debug( + `Creating report notification for grant ${grantId} at alertTime ${alertTime} (report deadline ${reportDeadline})`, + ); const message = `Report due in ${this.daysUntil(alertTime, reportDeadline)} days for ${organization}`; const notification: Notification = { notificationId: `${grantId}-report`, @@ -278,55 +309,82 @@ async makeGrantsInactive(grantId: number): Promise { await this.notificationService.createNotification(notification); } } + } else { + this.logger.debug( + `No report_deadlines configured for grant ${grantId}; skipping report notifications`, + ); } + + this.logger.log( + `Finished creating notifications for grant ${grantId} (${organization}) for user ${userId}`, + ); } - /** - * Helper method to update notifications for a grant's application and report deadlines - * @param grant represents the grant of which we want to update notifications for - */ + // Updates notifications for a grant's application and report deadlines private async updateGrantNotifications(grant: Grant) { const { grantId, organization, application_deadline, report_deadlines } = grant; - + this.logger.log( + `Updating notifications for grant ${grantId} (${organization})`, + ); + // Application notifications if (application_deadline) { + this.logger.debug( + `Updating application deadline notifications for grant ${grantId} with deadline ${application_deadline}`, + ); const alertTimes = this.getNotificationTimes(application_deadline); for (const alertTime of alertTimes) { const notificationId = `${grantId}-app`; const message = `Application due in ${this.daysUntil(alertTime, application_deadline)} days for ${organization}`; - + + this.logger.debug( + `Updating application notification ${notificationId} for grant ${grantId} to alertTime ${alertTime}`, + ); await this.notificationService.updateNotification(notificationId, { message, alertTime: alertTime as TDateISO, }); } + } else { + this.logger.debug( + `No application_deadline found for grant ${grantId}; skipping application notification updates`, + ); } - + // Report notifications if (report_deadlines && Array.isArray(report_deadlines)) { + this.logger.debug( + `Updating report deadline notifications for grant ${grantId} with ${report_deadlines.length} report_deadlines`, + ); for (const reportDeadline of report_deadlines) { const alertTimes = this.getNotificationTimes(reportDeadline); for (const alertTime of alertTimes) { const notificationId = `${grantId}-report`; const message = `Report due in ${this.daysUntil(alertTime, reportDeadline)} days for ${organization}`; - + + this.logger.debug( + `Updating report notification ${notificationId} for grant ${grantId} to alertTime ${alertTime} (report deadline ${reportDeadline})`, + ); await this.notificationService.updateNotification(notificationId, { message, alertTime: alertTime as TDateISO, }); } } + } else { + this.logger.debug( + `No report_deadlines configured for grant ${grantId}; skipping report notification updates`, + ); } + + this.logger.log( + `Finished updating notifications for grant ${grantId} (${organization})`, + ); } - - /* - Helper method that calculates the number of days between alert time and deadline - */ + + // Calculates the number of days between an alert time and deadline private daysUntil(alertTime: string, deadline: string): number { const diffMs = +new Date(deadline) - +new Date(alertTime); return Math.round(diffMs / (1000 * 60 * 60 * 24)); } - - - } \ No newline at end of file diff --git a/backend/src/grant/types/grant.types.ts b/backend/src/grant/types/grant.types.ts new file mode 100644 index 0000000..2d090a2 --- /dev/null +++ b/backend/src/grant/types/grant.types.ts @@ -0,0 +1,149 @@ +import { Grant } from '../../../../middle-layer/types/Grant'; +import { ApiProperty } from '@nestjs/swagger'; + +export class GrantResponseDto { + @ApiProperty({ description: 'Unique ID for the grant', example: 1234567890 }) + grantId!: number; + + @ApiProperty({ description: 'Organization giving the grant', example: 'Example Foundation' }) + organization!: string; + + @ApiProperty({ description: 'Whether BCAN qualifies for this grant', example: true }) + does_bcan_qualify!: boolean; + + @ApiProperty({ description: 'Current status of the grant', example: 'Active' }) + status!: string; + + @ApiProperty({ description: 'Amount of money given by the grant', example: 50000 }) + amount!: number; + + @ApiProperty({ description: 'When the grant money will start being issued', example: '2024-01-01T00:00:00.000Z' }) + grant_start_date!: string; + + @ApiProperty({ description: 'When grant submission is due', example: '2024-06-01T00:00:00.000Z' }) + application_deadline!: string; + + @ApiProperty({ description: 'Multiple report dates', type: [String], required: false }) + report_deadlines?: string[]; + + @ApiProperty({ description: 'Additional information about the grant', required: false }) + description?: string; + + @ApiProperty({ description: 'How long the grant will last in years', example: 1 }) + timeline!: number; + + @ApiProperty({ description: 'Estimated time to complete the grant application in hours', example: 40 }) + estimated_completion_time!: number; + + @ApiProperty({ description: 'Person of contact at organization giving the grant', required: false }) + grantmaker_poc?: any; + + @ApiProperty({ description: 'Person of contact at BCAN' }) + bcan_poc!: any; + + @ApiProperty({ description: 'Attachments related to the grant', type: [Object] }) + attachments!: any[]; + + @ApiProperty({ description: 'Whether the grant is restricted or unrestricted', example: false }) + isRestricted!: boolean; +} + +export class InactivateGrantBody { + @ApiProperty({ + description: 'Array of grant IDs to inactivate', + type: [Number], + example: [1234567890, 1234567891] + }) + grantIds!: number[]; +} + +export class AddGrantBody { + @ApiProperty({ description: 'Organization giving the grant', example: 'Example Foundation' }) + organization!: string; + + @ApiProperty({ description: 'Whether BCAN qualifies for this grant', example: true }) + does_bcan_qualify!: boolean; + + @ApiProperty({ description: 'Current status of the grant', example: 'Active' }) + status!: string; + + @ApiProperty({ description: 'Amount of money given by the grant', example: 50000 }) + amount!: number; + + @ApiProperty({ description: 'When the grant money will start being issued', example: '2024-01-01T00:00:00.000Z' }) + grant_start_date!: string; + + @ApiProperty({ description: 'When grant submission is due', example: '2024-06-01T00:00:00.000Z' }) + application_deadline!: string; + + @ApiProperty({ description: 'Multiple report dates', type: [String], required: false, example: ['2024-12-01T00:00:00.000Z'] }) + report_deadlines?: string[]; + + @ApiProperty({ description: 'Additional information about the grant', required: false, example: 'Grant for research purposes' }) + description?: string; + + @ApiProperty({ description: 'How long the grant will last in years', example: 1 }) + timeline!: number; + + @ApiProperty({ description: 'Estimated time to complete the grant application in hours', example: 40 }) + estimated_completion_time!: number; + + @ApiProperty({ description: 'Person of contact at organization giving the grant', required: false }) + grantmaker_poc?: any; + + @ApiProperty({ description: 'Person of contact at BCAN' }) + bcan_poc!: any; + + @ApiProperty({ description: 'Attachments related to the grant', type: [Object] }) + attachments!: any[]; + + @ApiProperty({ description: 'Whether the grant is restricted (specific purpose) or unrestricted', example: false }) + isRestricted!: boolean; +} + +export class UpdateGrantBody { + @ApiProperty({ description: 'Unique ID for the grant', example: 1234567890 }) + grantId!: number; + + @ApiProperty({ description: 'Organization giving the grant', example: 'Example Foundation', required: false }) + organization?: string; + + @ApiProperty({ description: 'Whether BCAN qualifies for this grant', required: false }) + does_bcan_qualify?: boolean; + + @ApiProperty({ description: 'Current status of the grant', required: false }) + status?: string; + + @ApiProperty({ description: 'Amount of money given by the grant', required: false }) + amount?: number; + + @ApiProperty({ description: 'When the grant money will start being issued', required: false }) + grant_start_date?: string; + + @ApiProperty({ description: 'When grant submission is due', required: false }) + application_deadline?: string; + + @ApiProperty({ description: 'Multiple report dates', type: [String], required: false }) + report_deadlines?: string[]; + + @ApiProperty({ description: 'Additional information about the grant', required: false }) + description?: string; + + @ApiProperty({ description: 'How long the grant will last in years', required: false }) + timeline?: number; + + @ApiProperty({ description: 'Estimated time to complete the grant application in hours', required: false }) + estimated_completion_time?: number; + + @ApiProperty({ description: 'Person of contact at organization giving the grant', required: false }) + grantmaker_poc?: any; + + @ApiProperty({ description: 'Person of contact at BCAN', required: false }) + bcan_poc?: any; + + @ApiProperty({ description: 'Attachments related to the grant', type: [Object], required: false }) + attachments?: any[]; + + @ApiProperty({ description: 'Whether the grant is restricted or unrestricted', required: false }) + isRestricted?: boolean; +} diff --git a/frontend/src/main-page/header/styles/AccountInfo.css b/frontend/src/main-page/header/styles/AccountInfo.css index e2c2894..11eb4f2 100644 --- a/frontend/src/main-page/header/styles/AccountInfo.css +++ b/frontend/src/main-page/header/styles/AccountInfo.css @@ -1,6 +1,6 @@ .account-modal { - position: fixed; + position: absolute; top: 70px; right: 20px; z-index: 9999; diff --git a/package.json b/package.json new file mode 100644 index 0000000..665732c --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@nestjs/swagger": "^11.2.5", + "swagger-ui-express": "^5.0.1" + } +}