Skip to content

Commit ed334bd

Browse files
authored
Merge pull request #73 from ModusCreateOrg/ADE-31-reports-page
[ADE-31] Reports and reports details page
2 parents 02c5637 + 612a674 commit ed334bd

File tree

18 files changed

+1440
-301
lines changed

18 files changed

+1440
-301
lines changed

frontend/src/common/api/__tests__/reportService.test.ts

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,12 @@ vi.mock('axios', () => ({
1919
// Mock dynamic imports to handle the service functions
2020
vi.mock('../reportService', async (importOriginal) => {
2121
const actual = await importOriginal() as typeof ReportServiceModule;
22-
22+
2323
// Create a new object with the same properties as the original
2424
return {
2525
// Keep the ReportError class
2626
ReportError: actual.ReportError,
27-
27+
2828
// Mock the API functions
2929
uploadReport: async (file: File, onProgress?: (progress: number) => void) => {
3030
try {
@@ -38,36 +38,36 @@ vi.mock('../reportService', async (importOriginal) => {
3838
return response.data;
3939
} catch (error) {
4040
// Properly wrap the error in a ReportError
41-
throw new actual.ReportError(error instanceof Error
41+
throw new actual.ReportError(error instanceof Error
4242
? `Failed to upload report: ${error.message}`
4343
: 'Failed to upload report');
4444
}
4545
},
46-
46+
4747
// Mock fetchLatestReports
4848
fetchLatestReports: async (limit = 3) => {
4949
try {
5050
const response = await axios.get(`/api/reports/latest?limit=${limit}`);
5151
return response.data;
5252
} catch (error) {
53-
throw new actual.ReportError(error instanceof Error
53+
throw new actual.ReportError(error instanceof Error
5454
? `Failed to fetch latest reports: ${error.message}`
5555
: 'Failed to fetch latest reports');
5656
}
5757
},
58-
58+
5959
// Mock fetchAllReports
6060
fetchAllReports: async () => {
6161
try {
6262
const response = await axios.get(`/api/reports`);
6363
return response.data;
6464
} catch (error) {
65-
throw new actual.ReportError(error instanceof Error
65+
throw new actual.ReportError(error instanceof Error
6666
? `Failed to fetch all reports: ${error.message}`
6767
: 'Failed to fetch all reports');
6868
}
6969
},
70-
70+
7171
// Keep other functions as is
7272
markReportAsRead: actual.markReportAsRead,
7373
getAuthConfig: actual.getAuthConfig,
@@ -92,28 +92,26 @@ const mockReports = [
9292
title: 'heart-report',
9393
status: ReportStatus.UNREAD,
9494
category: ReportCategory.HEART,
95-
documentUrl: 'http://example.com/heart-report.pdf',
9695
date: '2024-03-24',
9796
},
9897
{
9998
id: '2',
10099
title: 'brain-scan',
101100
status: ReportStatus.UNREAD,
102101
category: ReportCategory.NEUROLOGICAL,
103-
documentUrl: 'http://example.com/brain-scan.pdf',
104102
date: '2024-03-24',
105103
}
106104
];
107105

108106
describe('reportService', () => {
109107
const mockFile = new File(['test content'], 'test-report.pdf', { type: 'application/pdf' });
110108
let progressCallback: (progress: number) => void;
111-
109+
112110
beforeEach(() => {
113111
vi.resetAllMocks();
114112
progressCallback = vi.fn();
115113
});
116-
114+
117115
describe('uploadReport', () => {
118116
beforeEach(() => {
119117
// Mock axios.post for successful response
@@ -124,23 +122,22 @@ describe('reportService', () => {
124122
status: ReportStatus.UNREAD,
125123
category: ReportCategory.GENERAL,
126124
date: '2024-05-10',
127-
documentUrl: 'http://example.com/test-report.pdf'
128125
}
129126
});
130127
});
131-
128+
132129
test('should upload file successfully', async () => {
133130
const report = await uploadReport(mockFile, progressCallback);
134-
131+
135132
// Check the returned data matches our expectations
136133
expect(report).toBeDefined();
137134
expect(report.title).toBe('test-report');
138135
expect(report.status).toBe(ReportStatus.UNREAD);
139-
136+
140137
// Check the progress callback was called
141138
expect(progressCallback).toHaveBeenCalled();
142139
});
143-
140+
144141
test('should determine category based on filename', async () => {
145142
// Mock response for heart file
146143
(axios.post as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
@@ -150,14 +147,13 @@ describe('reportService', () => {
150147
status: ReportStatus.UNREAD,
151148
category: ReportCategory.HEART,
152149
date: '2024-05-10',
153-
documentUrl: 'http://example.com/heart-report.pdf'
154150
}
155151
});
156-
152+
157153
const heartFile = new File(['test'], 'heart-report.pdf', { type: 'application/pdf' });
158154
const heartReport = await uploadReport(heartFile);
159155
expect(heartReport.category).toBe(ReportCategory.HEART);
160-
156+
161157
// Mock response for neurological file
162158
(axios.post as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
163159
data: {
@@ -166,27 +162,26 @@ describe('reportService', () => {
166162
status: ReportStatus.UNREAD,
167163
category: ReportCategory.NEUROLOGICAL,
168164
date: '2024-05-10',
169-
documentUrl: 'http://example.com/brain-scan.pdf'
170165
}
171166
});
172-
167+
173168
const neuroFile = new File(['test'], 'brain-scan.pdf', { type: 'application/pdf' });
174169
const neuroReport = await uploadReport(neuroFile);
175170
expect(neuroReport.category).toBe(ReportCategory.NEUROLOGICAL);
176171
});
177-
172+
178173
test('should handle upload without progress callback', async () => {
179174
const report = await uploadReport(mockFile);
180175
expect(report).toBeDefined();
181176
expect(report.title).toBe('test-report');
182177
});
183-
178+
184179
test('should throw ReportError on upload failure', async () => {
185180
// Mock axios.post to fail
186181
(axios.post as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
187182
new Error('API request failed')
188183
);
189-
184+
190185
await expect(uploadReport(mockFile, progressCallback))
191186
.rejects
192187
.toThrow(ReportError);
@@ -203,30 +198,30 @@ describe('reportService', () => {
203198

204199
test('should fetch latest reports with default limit', async () => {
205200
const reports = await fetchLatestReports();
206-
201+
207202
expect(axios.get).toHaveBeenCalled();
208203
expect(reports).toHaveLength(2);
209204
expect(reports[0]).toEqual(expect.objectContaining({
210205
id: expect.any(String),
211206
title: expect.any(String)
212207
}));
213208
});
214-
209+
215210
test('should fetch latest reports with custom limit', async () => {
216211
const limit = 1;
217212
(axios.get as ReturnType<typeof vi.fn>).mockResolvedValue({
218213
data: mockReports.slice(0, 1)
219214
});
220-
215+
221216
const reports = await fetchLatestReports(limit);
222-
217+
223218
expect(axios.get).toHaveBeenCalled();
224219
expect(reports).toHaveLength(1);
225220
});
226-
221+
227222
test('should throw ReportError on fetch failure', async () => {
228223
(axios.get as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Network error'));
229-
224+
230225
await expect(fetchLatestReports())
231226
.rejects
232227
.toThrow(ReportError);
@@ -242,14 +237,14 @@ describe('reportService', () => {
242237

243238
test('should fetch all reports', async () => {
244239
const reports = await fetchAllReports();
245-
240+
246241
expect(axios.get).toHaveBeenCalled();
247242
expect(reports).toEqual(mockReports);
248243
});
249-
244+
250245
test('should throw ReportError on fetch failure', async () => {
251246
(axios.get as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Network error'));
252-
247+
253248
await expect(fetchAllReports())
254249
.rejects
255250
.toThrow(ReportError);
@@ -262,25 +257,25 @@ describe('reportService', () => {
262257
...mockReports[0],
263258
status: ReportStatus.READ
264259
};
265-
260+
266261
(axios.patch as ReturnType<typeof vi.fn>).mockResolvedValue({
267262
data: updatedReport
268263
});
269264
});
270265

271266
test('should mark a report as read', async () => {
272267
const updatedReport = await markReportAsRead('1');
273-
268+
274269
expect(axios.patch).toHaveBeenCalled();
275270
expect(updatedReport.status).toBe(ReportStatus.READ);
276271
});
277-
272+
278273
test('should throw error when report not found', async () => {
279274
(axios.patch as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Report not found'));
280-
275+
281276
await expect(markReportAsRead('non-existent-id'))
282277
.rejects
283278
.toThrow(ReportError);
284279
});
285280
});
286-
});
281+
});

frontend/src/common/api/reportService.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export const uploadReport = async (
6666

6767
// Send the report metadata to the API
6868
const response = await axios.post(
69-
`${API_URL}/api/reports`,
69+
`${API_URL}/api/reports`,
7070
{
7171
filePath: s3Key,
7272
},
@@ -142,3 +142,37 @@ export const markReportAsRead = async (reportId: string): Promise<MedicalReport>
142142
throw new ReportError('Failed to mark report as read');
143143
}
144144
};
145+
146+
/**
147+
* Toggles the bookmark status of a report.
148+
* @param reportId - ID of the report to toggle bookmark status
149+
* @param isBookmarked - Boolean indicating if the report should be bookmarked or not
150+
* @returns Promise with the updated report
151+
*/
152+
export const toggleReportBookmark = async (reportId: string, isBookmarked: boolean): Promise<MedicalReport> => {
153+
try {
154+
await axios.patch(`${API_URL}/api/reports/${reportId}/bookmark`, {
155+
bookmarked: isBookmarked
156+
}, await getAuthConfig());
157+
158+
// In a real implementation, this would return the response from the API
159+
// return response.data;
160+
161+
// For now, we'll mock the response
162+
const report = mockReports.find(r => r.id === reportId);
163+
164+
if (!report) {
165+
throw new Error(`Report with ID ${reportId} not found`);
166+
}
167+
168+
// Update the bookmark status
169+
report.bookmarked = isBookmarked;
170+
171+
return { ...report };
172+
} catch (error) {
173+
if (axios.isAxiosError(error)) {
174+
throw new ReportError(`Failed to toggle bookmark status: ${error.message}`);
175+
}
176+
throw new ReportError('Failed to toggle bookmark status');
177+
}
178+
};

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import ProfilePage from 'pages/Account/components/Profile/ProfilePage';
1616
import DiagnosticsPage from 'pages/Account/components/Diagnostics/DiagnosticsPage';
1717
import ChatPage from 'pages/Chat/ChatPage';
1818
import UploadPage from 'pages/Upload/UploadPage';
19+
import ReportDetailPage from 'pages/Reports/ReportDetailPage';
20+
import ReportsListPage from 'pages/Reports/ReportsListPage';
1921

2022
/**
2123
* The `TabNavigation` component provides a router outlet for all of the
@@ -82,6 +84,12 @@ const TabNavigation = (): JSX.Element => {
8284
<Route exact path="/tabs/upload">
8385
<UploadPage />
8486
</Route>
87+
<Route exact path="/tabs/reports">
88+
<ReportsListPage />
89+
</Route>
90+
<Route exact path="/tabs/reports/:reportId">
91+
<ReportDetailPage />
92+
</Route>
8593
<Route exact path="/">
8694
<Redirect to="/tabs/home" />
8795
</Route>
@@ -96,7 +104,7 @@ const TabNavigation = (): JSX.Element => {
96104
fixedWidth
97105
/>
98106
</IonTabButton>
99-
<IonTabButton className="ls-tab-navigation__bar-button" tab="reports" href="/reports">
107+
<IonTabButton className="ls-tab-navigation__bar-button" tab="reports" href="/tabs/reports">
100108
<Icon
101109
className="ls-tab-navigation__bar-button-icon"
102110
icon="fileLines"

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ describe('TabNavigation', () => {
222222
expect(screen.getByTestId('mock-ion-tab-button-home')).toHaveAttribute('data-href', '/tabs/home');
223223

224224
// Check for analytics tab button
225-
expect(screen.getByTestId('mock-ion-tab-button-reports')).toHaveAttribute('data-href', '/reports');
225+
expect(screen.getByTestId('mock-ion-tab-button-reports')).toHaveAttribute('data-href', '/tabs/reports');
226226

227227
// Check for chat tab button
228228
expect(screen.getByTestId('mock-ion-tab-button-chat')).toHaveAttribute('data-href', '/tabs/chat');

frontend/src/common/models/medicalReport.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,38 @@ export enum ReportCategory {
1515
* Status of a medical report.
1616
*/
1717
export enum ReportStatus {
18-
READ = 'read',
19-
UNREAD = 'unread'
18+
READ = 'READ',
19+
UNREAD = 'UNREAD'
20+
}
21+
22+
/**
23+
* Interface for report lab values.
24+
*/
25+
export interface LabValue {
26+
name: string;
27+
value: string;
28+
unit: string;
29+
normalRange: string;
30+
status: 'normal' | 'high' | 'low';
31+
isCritical: boolean;
32+
conclusion: string;
33+
suggestions: string;
2034
}
2135

2236
/**
2337
* Interface representing a medical report.
2438
*/
2539
export interface MedicalReport {
2640
id: string;
41+
userId: string;
2742
title: string;
28-
category: ReportCategory;
29-
createdAt: string; // ISO date string
43+
category: ReportCategory | string;
44+
bookmarked: boolean;
45+
isProcessed: boolean;
46+
labValues: LabValue[];
47+
summary: string;
3048
status: ReportStatus;
31-
}
49+
filePath: string;
50+
createdAt: string; // ISO date string
51+
updatedAt: string; // ISO date string
52+
}

frontend/src/common/utils/i18n/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ i18n
1919
// languages, namespaces, and resources
2020
supportedLngs: ['en', 'es', 'fr'],
2121
fallbackLng: 'en',
22-
ns: ['account', 'auth', 'common', 'errors', 'home', 'user'],
22+
ns: ['account', 'auth', 'common', 'errors', 'home', 'report', 'user'],
2323
defaultNS: 'common',
2424
resources: { en, es, fr },
2525

0 commit comments

Comments
 (0)