Skip to content

Commit d9a14c1

Browse files
authored
Merge pull request #31 from ModusCreateOrg/ADE-56
[ADE-56] - Upload Medical Report File for Easy Access
2 parents 93f6aff + 26f2e4e commit d9a14c1

29 files changed

+2432
-44
lines changed

Diff for: .cursor/rules/general.mdc

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
description: Follow this rules for every request
33
globs:
4+
alwaysApply: false
45
---
56

67
- Project Proposal Overview: This project proposes an AI-powered medical report translator that simplifies complex medical documents for patients and caregivers. By leveraging AI-driven text extraction and natural language processing (NLP), the system translates medical jargon into plain language, helping users understand their health conditions, diagnoses, and test results without relying on unreliable online searches.
@@ -67,7 +68,10 @@ Technologies:
6768
[5 - Results analysis.png](mdc:docs/assets/images/5 - Results analysis.png)
6869
[6 - Results Archive.png](mdc:docs/assets/images/6 - Results Archive.png)
6970
[7 - Detail.png](mdc:docs/assets/images/7 - Detail.png)
70-
71+
[Upload_default.png](mdc:docs/assets/images/Upload_default.png)
72+
[Upload_success.png](mdc:docs/assets/images/Upload_success.png)
73+
[Uploading.png](mdc:docs/assets/images/Uploading.png)
74+
[Uploading_complete.png](mdc:docs/assets/images/Uploading_complete.png)
7175

7276
AWS architecture: [aws architecture.pdf](mdc:docs/assets/aws architecture.pdf)
7377

@@ -121,10 +125,37 @@ AWS architecture: [aws architecture.pdf](mdc:docs/assets/aws architecture.pdf)
121125
```
122126
```
123127

128+
# General Code Guidelines
129+
130+
## Category Determination Pattern
131+
When determining categories based on keywords in filenames or text:
132+
133+
1. Define a constant mapping object at module level that maps categories to their identifying keywords
134+
2. Use TypeScript's Record type to ensure type safety
135+
3. Convert input to lowercase once at the start
136+
4. Use Array methods like `find` and `some` for clean keyword matching
137+
5. Provide a default/fallback category
138+
6. Include JSDoc with clear parameter and return descriptions
139+
140+
Example:
141+
```typescript
142+
const CATEGORY_KEYWORDS: Record<Category, string[]> = {
143+
[Category.TYPE_A]: ['keyword1', 'keyword2'],
144+
[Category.TYPE_B]: ['keyword3', 'keyword4'],
145+
[Category.DEFAULT]: []
146+
};
147+
148+
const determineCategory = (input: string): Category => {
149+
const lowerInput = input.toLowerCase();
150+
const matchedCategory = Object.entries(CATEGORY_KEYWORDS)
151+
.find(([_, keywords]) => keywords.some(k => lowerInput.includes(k)));
152+
return matchedCategory ? (matchedCategory[0] as Category) : Category.DEFAULT;
153+
};
154+
```
155+
124156
# Typescript rules
125157

126158
- Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.
127159

128160
This rule provides clear guidelines on what units to use, how to convert between units, and why it's important for your project. You can add this to your general rules to ensure consistency across the codebase.
129161

130-
Prefer using nullish coalescing operator (`??`) instead of a logical or (`||`), as it is a safer operator.

Diff for: docs/assets/images/Upload_default.png

111 KB
Loading

Diff for: docs/assets/images/Upload_success.png

104 KB
Loading

Diff for: docs/assets/images/Uploading.png

106 KB
Loading

Diff for: docs/assets/images/Uploading_complete.png

104 KB
Loading

Diff for: frontend/capacitor.config.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { CapacitorConfig } from '@capacitor/cli';
22

33
const config: CapacitorConfig = {
4-
appId: 'net.leanstacks.ionic8',
5-
appName: 'Ionic Playground 8',
4+
appId: 'com.moduscreate.medreportai',
5+
appName: 'MedReportAI',
66
webDir: 'dist',
77
plugins: {
88
StatusBar: {
@@ -17,6 +17,11 @@ const config: CapacitorConfig = {
1717
android: {
1818
// Handle status bar color/transparency on Android
1919
backgroundColor: '#4765ff'
20+
},
21+
server: {
22+
allowNavigation: [
23+
"https://*"
24+
]
2025
}
2126
};
2227

Diff for: frontend/package-lock.json

+11-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: frontend/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@
4242
"@capacitor/app": "6.0.2",
4343
"@capacitor/assets": "3.0.5",
4444
"@capacitor/core": "6.2.0",
45+
"@capacitor/filesystem": "6.0.2",
4546
"@capacitor/haptics": "6.0.2",
46-
"@capacitor/ios": "^6.2.0",
47+
"@capacitor/ios": "6.2.0",
4748
"@capacitor/keyboard": "6.0.3",
4849
"@capacitor/status-bar": "6.0.2",
4950
"@fortawesome/fontawesome-svg-core": "6.7.2",
+232
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { vi, describe, test, expect, beforeEach, afterEach } from 'vitest';
2+
import { uploadReport, ReportError, fetchLatestReports, fetchAllReports, markReportAsRead } from '../reportService';
3+
import { ReportCategory, ReportStatus } from '../../models/medicalReport';
4+
import axios from 'axios';
5+
6+
// Mock axios
7+
vi.mock('axios', () => ({
8+
default: {
9+
post: vi.fn(),
10+
get: vi.fn(),
11+
patch: vi.fn(),
12+
isAxiosError: vi.fn(() => true)
13+
}
14+
}));
15+
16+
// Mock response data
17+
const mockReports = [
18+
{
19+
id: '1',
20+
title: 'heart-report',
21+
status: ReportStatus.UNREAD,
22+
category: ReportCategory.HEART,
23+
documentUrl: 'http://example.com/heart-report.pdf',
24+
date: '2024-03-24',
25+
},
26+
{
27+
id: '2',
28+
title: 'brain-scan',
29+
status: ReportStatus.UNREAD,
30+
category: ReportCategory.NEUROLOGICAL,
31+
documentUrl: 'http://example.com/brain-scan.pdf',
32+
date: '2024-03-24',
33+
}
34+
];
35+
36+
describe('reportService', () => {
37+
const mockFile = new File(['test content'], 'test-report.pdf', { type: 'application/pdf' });
38+
let progressCallback: (progress: number) => void;
39+
40+
beforeEach(() => {
41+
vi.resetAllMocks();
42+
progressCallback = vi.fn();
43+
});
44+
45+
describe('uploadReport', () => {
46+
// Create a mock implementation for FormData
47+
let mockFormData: { append: ReturnType<typeof vi.fn> };
48+
49+
beforeEach(() => {
50+
// Mock the internal timers used in uploadReport
51+
vi.spyOn(global, 'setTimeout').mockImplementation((fn) => {
52+
if (typeof fn === 'function') fn();
53+
return 123 as unknown as NodeJS.Timeout;
54+
});
55+
56+
vi.spyOn(global, 'setInterval').mockImplementation(() => {
57+
return 456 as unknown as NodeJS.Timeout;
58+
});
59+
60+
vi.spyOn(global, 'clearInterval').mockImplementation(() => {});
61+
62+
// Setup mock FormData
63+
mockFormData = {
64+
append: vi.fn()
65+
};
66+
67+
// Mock FormData constructor
68+
global.FormData = vi.fn(() => mockFormData as unknown as FormData);
69+
});
70+
71+
test('should upload file successfully', async () => {
72+
const report = await uploadReport(mockFile, progressCallback);
73+
74+
// Check the returned data matches our expectations
75+
expect(report).toBeDefined();
76+
expect(report.title).toBe('test-report');
77+
expect(report.status).toBe(ReportStatus.UNREAD);
78+
79+
// Verify form data was created with the correct file
80+
expect(FormData).toHaveBeenCalled();
81+
expect(mockFormData.append).toHaveBeenCalledWith('file', mockFile);
82+
83+
// Check the progress callback was called
84+
expect(progressCallback).toHaveBeenCalled();
85+
});
86+
87+
test('should determine category based on filename', async () => {
88+
const heartFile = new File(['test'], 'heart-report.pdf', { type: 'application/pdf' });
89+
const heartReport = await uploadReport(heartFile);
90+
expect(heartReport.category).toBe(ReportCategory.HEART);
91+
92+
// Reset mocks for the second file
93+
vi.resetAllMocks();
94+
mockFormData = { append: vi.fn() };
95+
global.FormData = vi.fn(() => mockFormData as unknown as FormData);
96+
97+
// Recreate timer mocks for the second upload
98+
vi.spyOn(global, 'setTimeout').mockImplementation((fn) => {
99+
if (typeof fn === 'function') fn();
100+
return 123 as unknown as NodeJS.Timeout;
101+
});
102+
103+
vi.spyOn(global, 'setInterval').mockImplementation(() => {
104+
return 456 as unknown as NodeJS.Timeout;
105+
});
106+
107+
vi.spyOn(global, 'clearInterval').mockImplementation(() => {});
108+
109+
const neuroFile = new File(['test'], 'brain-scan.pdf', { type: 'application/pdf' });
110+
const neuroReport = await uploadReport(neuroFile);
111+
expect(neuroReport.category).toBe(ReportCategory.NEUROLOGICAL);
112+
});
113+
114+
test('should handle upload without progress callback', async () => {
115+
const report = await uploadReport(mockFile);
116+
expect(report).toBeDefined();
117+
expect(report.title).toBe('test-report');
118+
});
119+
120+
test('should throw ReportError on upload failure', async () => {
121+
// Restore the original FormData
122+
const originalFormData = global.FormData;
123+
124+
// Mock FormData to throw an error
125+
global.FormData = vi.fn(() => {
126+
throw new Error('FormData construction failed');
127+
});
128+
129+
await expect(uploadReport(mockFile, progressCallback))
130+
.rejects
131+
.toThrow(ReportError);
132+
133+
// Restore the previous mock
134+
global.FormData = originalFormData;
135+
});
136+
137+
afterEach(() => {
138+
vi.restoreAllMocks();
139+
});
140+
});
141+
142+
describe('fetchLatestReports', () => {
143+
beforeEach(() => {
144+
// Setup axios mock response
145+
(axios.get as ReturnType<typeof vi.fn>).mockResolvedValue({
146+
data: mockReports.slice(0, 2)
147+
});
148+
});
149+
150+
test('should fetch latest reports with default limit', async () => {
151+
const reports = await fetchLatestReports();
152+
153+
expect(axios.get).toHaveBeenCalled();
154+
expect(reports).toHaveLength(2);
155+
expect(reports[0]).toEqual(expect.objectContaining({
156+
id: expect.any(String),
157+
title: expect.any(String)
158+
}));
159+
});
160+
161+
test('should fetch latest reports with custom limit', async () => {
162+
const limit = 1;
163+
(axios.get as ReturnType<typeof vi.fn>).mockResolvedValue({
164+
data: mockReports.slice(0, 1)
165+
});
166+
167+
const reports = await fetchLatestReports(limit);
168+
169+
expect(axios.get).toHaveBeenCalled();
170+
expect(reports).toHaveLength(1);
171+
});
172+
173+
test('should throw ReportError on fetch failure', async () => {
174+
(axios.get as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Network error'));
175+
176+
await expect(fetchLatestReports())
177+
.rejects
178+
.toThrow(ReportError);
179+
});
180+
});
181+
182+
describe('fetchAllReports', () => {
183+
beforeEach(() => {
184+
(axios.get as ReturnType<typeof vi.fn>).mockResolvedValue({
185+
data: mockReports
186+
});
187+
});
188+
189+
test('should fetch all reports', async () => {
190+
const reports = await fetchAllReports();
191+
192+
expect(axios.get).toHaveBeenCalled();
193+
expect(reports).toEqual(mockReports);
194+
});
195+
196+
test('should throw ReportError on fetch failure', async () => {
197+
(axios.get as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Network error'));
198+
199+
await expect(fetchAllReports())
200+
.rejects
201+
.toThrow(ReportError);
202+
});
203+
});
204+
205+
describe('markReportAsRead', () => {
206+
beforeEach(() => {
207+
const updatedReport = {
208+
...mockReports[0],
209+
status: ReportStatus.READ
210+
};
211+
212+
(axios.patch as ReturnType<typeof vi.fn>).mockResolvedValue({
213+
data: updatedReport
214+
});
215+
});
216+
217+
test('should mark a report as read', async () => {
218+
const updatedReport = await markReportAsRead('1');
219+
220+
expect(axios.patch).toHaveBeenCalled();
221+
expect(updatedReport.status).toBe(ReportStatus.READ);
222+
});
223+
224+
test('should throw error when report not found', async () => {
225+
(axios.patch as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('Report not found'));
226+
227+
await expect(markReportAsRead('non-existent-id'))
228+
.rejects
229+
.toThrow(ReportError);
230+
});
231+
});
232+
});

0 commit comments

Comments
 (0)