Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions backend/src/iac/backend-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,9 @@ export class BackendStack extends cdk.Stack {
// Create the 'status' resource under ':id'
const reportStatusResource = reportIdResource.addResource('status');

// Create the 'bookmark' resource under ':id'
const reportBookmarkResource = reportIdResource.addResource('bookmark');

// Create the 'status' resource under ':id'
const documentProcessorResource = apiResource.addResource('document-processor');
const processFileResource = documentProcessorResource.addResource('process-file');
Expand Down Expand Up @@ -460,6 +463,18 @@ export class BackendStack extends cdk.Stack {
},
});

const patchReportBookmarkIntegration = new apigateway.Integration({
type: apigateway.IntegrationType.HTTP_PROXY,
integrationHttpMethod: 'PATCH',
uri: `${serviceUrl}/api/reports/{id}/bookmark`,
options: {
...integrationOptions,
requestParameters: {
'integration.request.path.id': 'method.request.path.id',
},
},
});

const processFileIntegration = new apigateway.Integration({
type: apigateway.IntegrationType.HTTP_PROXY,
integrationHttpMethod: 'POST',
Expand Down Expand Up @@ -493,6 +508,14 @@ export class BackendStack extends cdk.Stack {
},
});

// Add method for bookmark endpoint
reportBookmarkResource.addMethod('PATCH', patchReportBookmarkIntegration, {
...methodOptions,
requestParameters: {
'method.request.path.id': true,
},
});

// Add POST method to process file
processFileResource.addMethod('POST', processFileIntegration, methodOptions);

Expand Down Expand Up @@ -526,6 +549,10 @@ export class BackendStack extends cdk.Stack {
...corsOptions,
allowCredentials: false,
});
reportBookmarkResource.addCorsPreflight({
...corsOptions,
allowCredentials: false,
});
docsResource.addCorsPreflight({
...corsOptions,
allowCredentials: false,
Expand Down
36 changes: 36 additions & 0 deletions backend/src/reports/reports.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,42 @@ export class ReportsController {
return this.reportsService.updateStatus(id, updateDto, userId);
}

@ApiOperation({ summary: 'Toggle report bookmark status' })
@ApiResponse({
status: 200,
description: 'Report bookmark status toggled successfully',
type: Report,
})
@ApiResponse({
status: 404,
description: 'Report not found',
})
@ApiParam({
name: 'id',
description: 'Report ID',
})
@ApiBody({
schema: {
type: 'object',
properties: {
bookmarked: {
type: 'boolean',
description: 'New bookmark status',
},
},
required: ['bookmarked'],
},
})
@Patch(':id/bookmark')
async toggleBookmark(
@Param('id') id: string,
@Body('bookmarked') bookmarked: boolean,
@Req() request: RequestWithUser,
): Promise<Report> {
const userId = this.extractUserId(request);
return this.reportsService.toggleBookmark(id, bookmarked, userId);
}

@ApiOperation({ summary: 'Create a new report from S3 file' })
@ApiResponse({
status: 201,
Expand Down
73 changes: 73 additions & 0 deletions backend/src/reports/reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,4 +379,77 @@ export class ReportsService {
throw new InternalServerErrorException(`Failed to update report with ID ${report.id}`);
}
}

/**
* Toggle the bookmark status of a report
* @param id Report ID
* @param bookmarked New bookmark status
* @param userId User ID
* @returns The updated report
*/
async toggleBookmark(id: string, bookmarked: boolean, userId: string): Promise<Report> {
if (!id) {
throw new NotFoundException('Report ID is required');
}

if (bookmarked === undefined) {
throw new InternalServerErrorException('Bookmark status is required');
}

if (!userId) {
throw new ForbiddenException('User ID is required');
}

try {
// First check if the report exists and belongs to the user
const existingReport = await this.findOne(id, userId);

const command = new UpdateItemCommand({
TableName: this.tableName,
Key: marshall({
userId,
id,
}),
UpdateExpression: 'SET bookmarked = :bookmarked, updatedAt = :updatedAt',
ExpressionAttributeValues: marshall({
':bookmarked': bookmarked,
':updatedAt': new Date().toISOString(),
':userId': userId,
}),
ReturnValues: 'ALL_NEW',
});

const response = await this.dynamoClient.send(command);

if (!response.Attributes) {
// If for some reason Attributes is undefined, return the existing report with updated bookmark status
return {
...existingReport,
bookmarked,
updatedAt: new Date().toISOString(),
};
}

return unmarshall(response.Attributes) as Report;
} catch (error: unknown) {
if (error instanceof NotFoundException) {
throw error;
}

this.logger.error(`Error toggling bookmark for report ID ${id}:`);
this.logger.error(error);

if (error instanceof DynamoDBServiceException) {
if (error.name === 'ConditionalCheckFailedException') {
throw new ForbiddenException('You do not have permission to update this report');
} else if (error.name === 'ResourceNotFoundException') {
throw new InternalServerErrorException(
`Table "${this.tableName}" not found. Please check your database configuration.`,
);
}
}

throw new InternalServerErrorException(`Failed to toggle bookmark for report ID ${id}`);
}
}
}
51 changes: 3 additions & 48 deletions frontend/src/common/api/reportService.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,9 @@
import axios, { AxiosProgressEvent } from 'axios';
import { MedicalReport, ReportCategory, ReportStatus, ProcessingStatus } from '../models/medicalReport';
import { MedicalReport } from '../models/medicalReport';
import { fetchAuthSession } from '@aws-amplify/auth';
// Get the API URL from environment variables
const API_URL = import.meta.env.VITE_BASE_URL_API || '';

// Mock data for testing and development
const mockReports: MedicalReport[] = [
{
id: '1',
userId: 'user1',
title: 'Blood Test Report',
category: ReportCategory.GENERAL,
bookmarked: false,
processingStatus: ProcessingStatus.PROCESSED,
labValues: [],
summary: 'Blood test results within normal range',
status: ReportStatus.UNREAD,
filePath: '/reports/blood-test.pdf',
createdAt: '2023-04-15T12:30:00Z',
updatedAt: '2023-04-15T12:30:00Z',
},
{
id: '2',
userId: 'user1',
title: 'Heart Checkup',
category: ReportCategory.HEART,
bookmarked: true,
processingStatus: ProcessingStatus.PROCESSED,
labValues: [],
summary: 'Heart functioning normally',
status: ReportStatus.READ,
filePath: '/reports/heart-checkup.pdf',
createdAt: '2023-04-10T10:15:00Z',
updatedAt: '2023-04-10T10:15:00Z',
},
];

/**
* Interface for upload progress callback
*/
Expand Down Expand Up @@ -195,28 +163,15 @@ export const toggleReportBookmark = async (
isBookmarked: boolean,
): Promise<MedicalReport> => {
try {
await axios.patch(
const response = await axios.patch(
`${API_URL}/api/reports/${reportId}/bookmark`,
{
bookmarked: isBookmarked,
},
await getAuthConfig(),
);

// In a real implementation, this would return the response from the API
// return response.data;

// For now, we'll mock the response
const report = mockReports.find((r) => r.id === reportId);

if (!report) {
throw new Error(`Report with ID ${reportId} not found`);
}

// Update the bookmark status
report.bookmarked = isBookmarked;

return { ...report };
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new ReportError(`Failed to toggle bookmark status: ${error.message}`);
Expand Down