Skip to content

Commit 4f49b38

Browse files
authored
Merge pull request #76 from ModusCreateOrg/ADE-66
[ADE-66] create the frontend of processing and showing the reports details
2 parents fd4482d + 034779b commit 4f49b38

File tree

9 files changed

+851
-239
lines changed

9 files changed

+851
-239
lines changed

backend/src/reports/reports.service.ts

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import {
88
import { ConfigService } from '@nestjs/config';
99
import {
1010
DynamoDBClient,
11-
ScanCommand,
1211
GetItemCommand,
1312
UpdateItemCommand,
1413
DynamoDBServiceException,
1514
PutItemCommand,
15+
QueryCommand,
1616
} from '@aws-sdk/client-dynamodb';
1717
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
1818
import { Report, ReportStatus } from './models/report.model';
@@ -60,10 +60,10 @@ export class ReportsService {
6060
}
6161

6262
try {
63-
// If the table has a GSI for userId, use QueryCommand instead
64-
const command = new ScanCommand({
63+
// Use QueryCommand instead of ScanCommand since userId is the partition key
64+
const command = new QueryCommand({
6565
TableName: this.tableName,
66-
FilterExpression: 'userId = :userId',
66+
KeyConditionExpression: 'userId = :userId',
6767
ExpressionAttributeValues: marshall({
6868
':userId': userId,
6969
}),
@@ -105,23 +105,21 @@ export class ReportsService {
105105
typeof queryDto.limit === 'string' ? parseInt(queryDto.limit, 10) : queryDto.limit || 10;
106106

107107
try {
108-
// If the table has a GSI for userId, use QueryCommand instead
109-
const command = new ScanCommand({
108+
// Use the GSI userIdCreatedAtIndex with QueryCommand for efficient retrieval
109+
// This is much more efficient than a ScanCommand
110+
const command = new QueryCommand({
110111
TableName: this.tableName,
111-
FilterExpression: 'userId = :userId',
112+
IndexName: 'userIdCreatedAtIndex', // Use the GSI for efficient queries
113+
KeyConditionExpression: 'userId = :userId',
112114
ExpressionAttributeValues: marshall({
113115
':userId': userId,
114116
}),
115-
Limit: limit * 5, // Fetch more items since we'll filter by userId
117+
ScanIndexForward: false, // Get items in descending order (newest first)
118+
Limit: limit, // Only fetch the number of items we need
116119
});
117120

118121
const response = await this.dynamoClient.send(command);
119-
const reports = (response.Items || []).map(item => unmarshall(item) as Report);
120-
121-
// Sort by createdAt in descending order
122-
return reports
123-
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
124-
.slice(0, limit);
122+
return (response.Items || []).map(item => unmarshall(item) as Report);
125123
} catch (error: unknown) {
126124
this.logger.error(`Error fetching latest reports for user ${userId}:`);
127125
this.logger.error(error);
@@ -131,6 +129,26 @@ export class ReportsService {
131129
throw new InternalServerErrorException(
132130
`Table "${this.tableName}" not found. Please check your database configuration.`,
133131
);
132+
} else if (error.name === 'ValidationException') {
133+
// This could happen if the GSI doesn't exist
134+
this.logger.warn('GSI validation error, falling back to standard query');
135+
136+
// Fallback to standard query and sort in memory if GSI has issues
137+
const fallbackCommand = new QueryCommand({
138+
TableName: this.tableName,
139+
KeyConditionExpression: 'userId = :userId',
140+
ExpressionAttributeValues: marshall({
141+
':userId': userId,
142+
}),
143+
});
144+
145+
const fallbackResponse = await this.dynamoClient.send(fallbackCommand);
146+
const reports = (fallbackResponse.Items || []).map(item => unmarshall(item) as Report);
147+
148+
// Sort by createdAt in descending order
149+
return reports
150+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
151+
.slice(0, limit);
134152
}
135153
}
136154

@@ -330,25 +348,75 @@ export class ReportsService {
330348
throw new ForbiddenException('User ID is required');
331349
}
332350

351+
// Log the actual filePath being searched for debugging
352+
this.logger.log(`Searching for report with filePath: "${filePath}" for user ${userId}`);
353+
333354
try {
334-
// Since filePath isn't a key attribute, we need to scan with filter
335-
const command = new ScanCommand({
355+
const command = new QueryCommand({
336356
TableName: this.tableName,
337-
FilterExpression: 'filePath = :filePath AND userId = :userId',
357+
KeyConditionExpression: 'userId = :userId',
358+
FilterExpression: 'filePath = :filePath',
338359
ExpressionAttributeValues: marshall({
339-
':filePath': filePath,
340360
':userId': userId,
361+
':filePath': filePath,
341362
}),
342363
Limit: 1, // We only want one record
343364
});
344365

366+
this.logger.log('Executing QueryCommand with params:', {
367+
TableName: this.tableName,
368+
KeyConditionExpression: 'userId = :userId',
369+
FilterExpression: 'filePath = :filePath',
370+
Values: {
371+
userId,
372+
filePath,
373+
},
374+
});
375+
345376
const response = await this.dynamoClient.send(command);
346377

378+
this.logger.log(`Query response received, found ${response.Items?.length || 0} items`);
379+
347380
if (!response.Items || response.Items.length === 0) {
381+
// If no exact match, try with case-insensitive comparison as a fallback
382+
this.logger.log('No exact match found, trying with case-insensitive search');
383+
384+
// Get all items for the user and filter manually for case-insensitive match
385+
const allUserItemsCommand = new QueryCommand({
386+
TableName: this.tableName,
387+
KeyConditionExpression: 'userId = :userId',
388+
ExpressionAttributeValues: marshall({
389+
':userId': userId,
390+
}),
391+
});
392+
393+
const allUserResponse = await this.dynamoClient.send(allUserItemsCommand);
394+
395+
if (!allUserResponse.Items || allUserResponse.Items.length === 0) {
396+
return null;
397+
}
398+
399+
// Convert items and find case-insensitive match
400+
const allReports = allUserResponse.Items.map(item => unmarshall(item) as Report);
401+
const matchingReport = allReports.find(
402+
report => report.filePath.toLowerCase() === filePath.toLowerCase(),
403+
);
404+
405+
if (matchingReport) {
406+
this.logger.log(
407+
`Found case-insensitive match for ${filePath}: ${matchingReport.filePath}`,
408+
);
409+
410+
return matchingReport;
411+
}
412+
348413
return null;
349414
}
350415

351-
return unmarshall(response.Items[0]) as Report;
416+
const result = unmarshall(response.Items[0]) as Report;
417+
this.logger.log(`Found report with ID ${result.id}`);
418+
419+
return result;
352420
} catch (error: unknown) {
353421
this.logger.error(`Error finding report with filePath ${filePath}:`);
354422
this.logger.error(error);

frontend/src/common/api/reportService.ts

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,41 @@
11
import axios, { AxiosProgressEvent } from 'axios';
2-
import { MedicalReport } from '../models/medicalReport';
2+
import { MedicalReport, ReportCategory, ReportStatus } from '../models/medicalReport';
33
import { fetchAuthSession } from '@aws-amplify/auth';
44
// Get the API URL from environment variables
55
const API_URL = import.meta.env.VITE_BASE_URL_API || '';
66

7+
// Mock data for testing and development
8+
const mockReports: MedicalReport[] = [
9+
{
10+
id: '1',
11+
userId: 'user1',
12+
title: 'Blood Test Report',
13+
category: ReportCategory.GENERAL,
14+
bookmarked: false,
15+
isProcessed: true,
16+
labValues: [],
17+
summary: 'Blood test results within normal range',
18+
status: ReportStatus.UNREAD,
19+
filePath: '/reports/blood-test.pdf',
20+
createdAt: '2023-04-15T12:30:00Z',
21+
updatedAt: '2023-04-15T12:30:00Z',
22+
},
23+
{
24+
id: '2',
25+
userId: 'user1',
26+
title: 'Heart Checkup',
27+
category: ReportCategory.HEART,
28+
bookmarked: true,
29+
isProcessed: true,
30+
labValues: [],
31+
summary: 'Heart functioning normally',
32+
status: ReportStatus.READ,
33+
filePath: '/reports/heart-checkup.pdf',
34+
createdAt: '2023-04-10T10:15:00Z',
35+
updatedAt: '2023-04-10T10:15:00Z',
36+
},
37+
];
38+
739
/**
840
* Interface for upload progress callback
941
*/
@@ -14,16 +46,22 @@ export interface UploadProgressCallback {
1446
/**
1547
* Creates an authenticated request config with bearer token
1648
*/
17-
export const getAuthConfig = async (signal?: AbortSignal): Promise<{ headers: { Accept: string, 'Content-Type': string, Authorization: string }, signal?: AbortSignal, onUploadProgress?: (progressEvent: AxiosProgressEvent) => void }> => {
49+
export const getAuthConfig = async (
50+
signal?: AbortSignal,
51+
): Promise<{
52+
headers: { Accept: string; 'Content-Type': string; Authorization: string };
53+
signal?: AbortSignal;
54+
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
55+
}> => {
1856
const session = await fetchAuthSession();
1957
const idToken = session.tokens?.idToken?.toString() || '';
2058
return {
21-
headers: {
22-
Accept: 'application/json',
23-
'Content-Type': 'application/json',
24-
Authorization: idToken ? `Bearer ${idToken}` : ''
25-
},
26-
signal
59+
headers: {
60+
Accept: 'application/json',
61+
'Content-Type': 'application/json',
62+
Authorization: idToken ? `Bearer ${idToken}` : '',
63+
},
64+
signal,
2765
};
2866
};
2967

@@ -47,7 +85,7 @@ export class ReportError extends Error {
4785
export const uploadReport = async (
4886
file: File,
4987
onProgress?: UploadProgressCallback,
50-
signal?: AbortSignal
88+
signal?: AbortSignal,
5189
): Promise<MedicalReport> => {
5290
try {
5391
// Import s3StorageService dynamically to avoid circular dependency
@@ -58,7 +96,7 @@ export const uploadReport = async (
5896
file,
5997
'reports',
6098
onProgress as (progress: number) => void,
61-
signal
99+
signal,
62100
);
63101

64102
// Then create the report record with the S3 key
@@ -70,7 +108,7 @@ export const uploadReport = async (
70108
{
71109
filePath: s3Key,
72110
},
73-
config
111+
config,
74112
);
75113

76114
return response.data;
@@ -79,7 +117,7 @@ export const uploadReport = async (
79117
if (signal?.aborted) {
80118
throw new DOMException('The operation was aborted', 'AbortError');
81119
}
82-
120+
83121
if (axios.isAxiosError(error)) {
84122
console.error('API Error Details:', error.response?.data, error.response?.headers);
85123
throw new ReportError(`Failed to upload report: ${error.message}`);
@@ -95,7 +133,10 @@ export const uploadReport = async (
95133
*/
96134
export const fetchLatestReports = async (limit = 3): Promise<MedicalReport[]> => {
97135
try {
98-
const response = await axios.get(`${API_URL}/api/reports/latest?limit=${limit}`, await getAuthConfig());
136+
const response = await axios.get(
137+
`${API_URL}/api/reports/latest?limit=${limit}`,
138+
await getAuthConfig(),
139+
);
99140
console.log('response', response.data);
100141
console.log('API_URL', API_URL);
101142
return response.data;
@@ -113,7 +154,7 @@ export const fetchLatestReports = async (limit = 3): Promise<MedicalReport[]> =>
113154
*/
114155
export const fetchAllReports = async (): Promise<MedicalReport[]> => {
115156
try {
116-
const response = await axios.get(`${API_URL}/api/reports`, await getAuthConfig() );
157+
const response = await axios.get(`${API_URL}/api/reports`, await getAuthConfig());
117158
return response.data;
118159
} catch (error) {
119160
if (axios.isAxiosError(error)) {
@@ -131,7 +172,7 @@ export const fetchAllReports = async (): Promise<MedicalReport[]> => {
131172
export const markReportAsRead = async (reportId: string): Promise<MedicalReport> => {
132173
try {
133174
const response = await axios.patch(`${API_URL}/api/reports/${reportId}`, {
134-
status: 'READ'
175+
status: 'READ',
135176
});
136177

137178
return response.data;
@@ -149,17 +190,24 @@ export const markReportAsRead = async (reportId: string): Promise<MedicalReport>
149190
* @param isBookmarked - Boolean indicating if the report should be bookmarked or not
150191
* @returns Promise with the updated report
151192
*/
152-
export const toggleReportBookmark = async (reportId: string, isBookmarked: boolean): Promise<MedicalReport> => {
193+
export const toggleReportBookmark = async (
194+
reportId: string,
195+
isBookmarked: boolean,
196+
): Promise<MedicalReport> => {
153197
try {
154-
await axios.patch(`${API_URL}/api/reports/${reportId}/bookmark`, {
155-
bookmarked: isBookmarked
156-
}, await getAuthConfig());
198+
await axios.patch(
199+
`${API_URL}/api/reports/${reportId}/bookmark`,
200+
{
201+
bookmarked: isBookmarked,
202+
},
203+
await getAuthConfig(),
204+
);
157205

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

161209
// For now, we'll mock the response
162-
const report = mockReports.find(r => r.id === reportId);
210+
const report = mockReports.find((r) => r.id === reportId);
163211

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

frontend/src/common/components/Router/TabNavigation.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import ChatPage from 'pages/Chat/ChatPage';
1818
import UploadPage from 'pages/Upload/UploadPage';
1919
import ReportDetailPage from 'pages/Reports/ReportDetailPage';
2020
import ReportsListPage from 'pages/Reports/ReportsListPage';
21+
import Processing from 'pages/Processing/Processing';
2122

2223
/**
2324
* The `TabNavigation` component provides a router outlet for all of the
@@ -90,6 +91,9 @@ const TabNavigation = (): JSX.Element => {
9091
<Route exact path="/tabs/reports/:reportId">
9192
<ReportDetailPage />
9293
</Route>
94+
<Route exact path="/tabs/processing">
95+
<Processing />
96+
</Route>
9397
<Route exact path="/">
9498
<Redirect to="/tabs/home" />
9599
</Route>

0 commit comments

Comments
 (0)