diff --git a/backend/src/reports/reports.controller.ts b/backend/src/reports/reports.controller.ts index 17b21a8e..707eb403 100644 --- a/backend/src/reports/reports.controller.ts +++ b/backend/src/reports/reports.controller.ts @@ -9,6 +9,7 @@ import { Req, UnauthorizedException, Post, + NotFoundException, } from '@nestjs/common'; import { ApiTags, @@ -20,7 +21,7 @@ import { ApiBody, } from '@nestjs/swagger'; import { ReportsService } from './reports.service'; -import { Report } from './models/report.model'; +import { ProcessingStatus, Report } from './models/report.model'; import { GetReportsQueryDto } from './dto/get-reports.dto'; import { UpdateReportStatusDto } from './dto/update-report-status.dto'; import { RequestWithUser } from '../auth/auth.middleware'; @@ -80,7 +81,17 @@ export class ReportsController { @Get(':id') async getReport(@Param('id') id: string, @Req() request: RequestWithUser): Promise { const userId = this.extractUserId(request); - return this.reportsService.findOne(id, userId); + const report = await this.reportsService.findOne(id, userId); + + if (!report) { + throw new NotFoundException('Report not found'); + } + + if (report.processingStatus === ProcessingStatus.FAILED) { + throw new NotFoundException('Processing failed'); + } + + return report; } @ApiOperation({ summary: 'Update report status' }) diff --git a/backend/src/reports/reports.service.ts b/backend/src/reports/reports.service.ts index f8681b3a..889e02db 100644 --- a/backend/src/reports/reports.service.ts +++ b/backend/src/reports/reports.service.ts @@ -13,6 +13,7 @@ import { DynamoDBServiceException, PutItemCommand, QueryCommand, + DeleteItemCommand, } from '@aws-sdk/client-dynamodb'; import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'; import { Report, ReportStatus, ProcessingStatus } from './models/report.model'; @@ -54,19 +55,24 @@ export class ReportsService { this.tableName = this.configService.get('dynamodbReportsTable')!; } - async findAll(userId: string): Promise { + async findAll(userId: string, withFailed = false): Promise { if (!userId) { throw new ForbiddenException('User ID is required'); } try { - // Use QueryCommand instead of ScanCommand since userId is the partition key + const expressionAttributeValues: any = { ':userId': userId }; + const processingStatusFilter = 'processingStatus <> :failedStatus'; + + if (!withFailed) { + expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED; + } + const command = new QueryCommand({ TableName: this.tableName, KeyConditionExpression: 'userId = :userId', - ExpressionAttributeValues: marshall({ - ':userId': userId, - }), + FilterExpression: !withFailed ? processingStatusFilter : undefined, + ExpressionAttributeValues: marshall(expressionAttributeValues), }); const response = await this.dynamoClient.send(command); @@ -91,7 +97,11 @@ export class ReportsService { } } - async findLatest(queryDto: GetReportsQueryDto, userId: string): Promise { + async findLatest( + queryDto: GetReportsQueryDto, + userId: string, + withFailed = false, + ): Promise { this.logger.log( `Running findLatest with params: ${JSON.stringify(queryDto)} for user ${userId}`, ); @@ -100,22 +110,26 @@ export class ReportsService { throw new ForbiddenException('User ID is required'); } - // Convert limit to a number to avoid serialization errors const limit = typeof queryDto.limit === 'string' ? parseInt(queryDto.limit, 10) : queryDto.limit || 10; + const expressionAttributeValues: any = { ':userId': userId }; + try { - // Use the GSI userIdCreatedAtIndex with QueryCommand for efficient retrieval - // This is much more efficient than a ScanCommand + const processingStatusFilter = 'processingStatus <> :failedStatus'; + + if (!withFailed) { + expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED; + } + const command = new QueryCommand({ TableName: this.tableName, - IndexName: 'userIdCreatedAtIndex', // Use the GSI for efficient queries + IndexName: 'userIdCreatedAtIndex', KeyConditionExpression: 'userId = :userId', - ExpressionAttributeValues: marshall({ - ':userId': userId, - }), - ScanIndexForward: false, // Get items in descending order (newest first) - Limit: limit, // Only fetch the number of items we need + FilterExpression: !withFailed ? processingStatusFilter : undefined, + ExpressionAttributeValues: marshall(expressionAttributeValues), + ScanIndexForward: false, + Limit: limit, }); const response = await this.dynamoClient.send(command); @@ -130,22 +144,17 @@ export class ReportsService { `Table "${this.tableName}" not found. Please check your database configuration.`, ); } else if (error.name === 'ValidationException') { - // This could happen if the GSI doesn't exist this.logger.warn('GSI validation error, falling back to standard query'); - // Fallback to standard query and sort in memory if GSI has issues const fallbackCommand = new QueryCommand({ TableName: this.tableName, KeyConditionExpression: 'userId = :userId', - ExpressionAttributeValues: marshall({ - ':userId': userId, - }), + ExpressionAttributeValues: marshall(expressionAttributeValues), }); const fallbackResponse = await this.dynamoClient.send(fallbackCommand); const reports = (fallbackResponse.Items || []).map(item => unmarshall(item) as Report); - // Sort by createdAt in descending order return reports .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) .slice(0, limit); @@ -469,4 +478,54 @@ export class ReportsService { throw new InternalServerErrorException(`Failed to toggle bookmark for report ID ${id}`); } } + + /** + * Delete a report by ID + * @param reportId Report ID + * @param userId User ID + * @returns A confirmation message + */ + async deleteReport(reportId: string, userId: string): Promise { + if (!reportId) { + throw new NotFoundException('Report ID is required'); + } + + if (!userId) { + throw new ForbiddenException('User ID is required'); + } + + try { + const command = new DeleteItemCommand({ + TableName: this.tableName, + Key: marshall({ + userId, + id: reportId, + }), + ConditionExpression: 'userId = :userId', + ExpressionAttributeValues: marshall({ + ':userId': userId, + }), + }); + + await this.dynamoClient.send(command); + this.logger.log(`Successfully deleted report with ID ${reportId} for user ${userId}`); + + return `Report with ID ${reportId} successfully deleted`; + } catch (error: unknown) { + this.logger.error(`Error deleting report with ID ${reportId}:`); + this.logger.error(error); + + if (error instanceof DynamoDBServiceException) { + if (error.name === 'ConditionalCheckFailedException') { + throw new ForbiddenException('You do not have permission to delete 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 delete report with ID ${reportId}`); + } + } } diff --git a/backend/src/services/perplexity.service.ts b/backend/src/services/perplexity.service.ts index fc0c5327..ab3751c7 100644 --- a/backend/src/services/perplexity.service.ts +++ b/backend/src/services/perplexity.service.ts @@ -158,7 +158,7 @@ export class PerplexityService { 'Your goal is to help patients understand their medical reports by translating medical jargon into plain language.\n' + 'You must be accurate, concise, comprehensive, and easy to understand. Use everyday analogies when helpful.\n'; - const userPrompt = `Please explain the following medical text in simple terms, in a single paragraph that's between 100 to 500 characters:\n\n${medicalText}`; + const userPrompt = `Please explain the following medical text in simple terms, in a single paragraph that's between 10 to 200 words, all in normal text NOT .md style, the more concise the better:\n\n${medicalText}`; const messages: PerplexityMessage[] = [ { role: 'system', content: systemPrompt }, diff --git a/frontend/src/pages/Reports/components/ReportHeader.tsx b/frontend/src/pages/Reports/components/ReportHeader.tsx index 00f2f2a5..849f6fda 100644 --- a/frontend/src/pages/Reports/components/ReportHeader.tsx +++ b/frontend/src/pages/Reports/components/ReportHeader.tsx @@ -25,7 +25,7 @@ const ReportHeader: React.FC = ({ reportData, onClose }) => { {/* Category & Title */}
- {reportData.category && t(`list.${reportData.category}Category`, { ns: 'report' })} + {reportData.category && t(`list.${reportData.category}Category`, { ns: 'reportDetail' })}