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
8 changes: 8 additions & 0 deletions backend/src/reports/reports.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ export class ReportsController {
throw new NotFoundException('Processing failed');
}

if (report.processingStatus === ProcessingStatus.IN_PROGRESS) {
throw new NotFoundException('Processing in progress');
}

if (report.processingStatus === ProcessingStatus.UNPROCESSED) {
throw new NotFoundException('Processing pending');
}

return report;
}

Expand Down
20 changes: 10 additions & 10 deletions backend/src/reports/reports.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,23 +55,23 @@ export class ReportsService {
this.tableName = this.configService.get<string>('dynamodbReportsTable')!;
}

async findAll(userId: string, withFailed = false): Promise<Report[]> {
async findAll(userId: string, onlyProcessed = true): Promise<Report[]> {
if (!userId) {
throw new ForbiddenException('User ID is required');
}

try {
const expressionAttributeValues: any = { ':userId': userId };
const processingStatusFilter = 'processingStatus <> :failedStatus';
const processingStatusFilter = 'processingStatus = :processedStatus';

if (!withFailed) {
expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED;
if (onlyProcessed) {
expressionAttributeValues[':processedStatus'] = ProcessingStatus.PROCESSED;
}

const command = new QueryCommand({
TableName: this.tableName,
KeyConditionExpression: 'userId = :userId',
FilterExpression: !withFailed ? processingStatusFilter : undefined,
FilterExpression: onlyProcessed ? processingStatusFilter : undefined,
ExpressionAttributeValues: marshall(expressionAttributeValues),
});

Expand Down Expand Up @@ -100,7 +100,7 @@ export class ReportsService {
async findLatest(
queryDto: GetReportsQueryDto,
userId: string,
withFailed = false,
onlyProcessed = true,
): Promise<Report[]> {
this.logger.log(
`Running findLatest with params: ${JSON.stringify(queryDto)} for user ${userId}`,
Expand All @@ -116,17 +116,17 @@ export class ReportsService {
const expressionAttributeValues: any = { ':userId': userId };

try {
const processingStatusFilter = 'processingStatus <> :failedStatus';
const processingStatusFilter = 'processingStatus = :processedStatus';

if (!withFailed) {
expressionAttributeValues[':failedStatus'] = ProcessingStatus.FAILED;
if (onlyProcessed) {
expressionAttributeValues[':processedStatus'] = ProcessingStatus.PROCESSED;
}

const command = new QueryCommand({
TableName: this.tableName,
IndexName: 'userIdCreatedAtIndex',
KeyConditionExpression: 'userId = :userId',
FilterExpression: !withFailed ? processingStatusFilter : undefined,
FilterExpression: onlyProcessed ? processingStatusFilter : undefined,
ExpressionAttributeValues: marshall(expressionAttributeValues),
ScanIndexForward: false,
Limit: limit,
Expand Down
49 changes: 35 additions & 14 deletions frontend/src/common/api/__tests__/reportService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ vi.mock('axios', () => ({
},
}));

// Mock auth
vi.mock('@aws-amplify/auth', () => ({
fetchAuthSession: vi.fn().mockResolvedValue({
tokens: {
idToken: {
toString: () => 'mock-id-token',
},
},
}),
}));

// Mock dynamic imports to handle the service functions
vi.mock('../reportService', async (importOriginal) => {
const actual = (await importOriginal()) as typeof ReportServiceModule;
Expand All @@ -31,6 +42,15 @@ vi.mock('../reportService', async (importOriginal) => {
// Keep the ReportError class
ReportError: actual.ReportError,

// Mock getAuthConfig to avoid authentication issues in tests
getAuthConfig: async () => ({
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer mock-id-token',
},
}),

// Mock the API functions
uploadReport: async (file: File, onProgress?: (progress: number) => void) => {
try {
Expand Down Expand Up @@ -80,23 +100,24 @@ vi.mock('../reportService', async (importOriginal) => {
}
},

// Keep other functions as is
markReportAsRead: actual.markReportAsRead,
getAuthConfig: actual.getAuthConfig,
// Mock markReportAsRead to avoid the dependency on getAuthConfig
markReportAsRead: async (reportId: string) => {
try {
const response = await axios.patch(`/api/reports/${reportId}`, {
status: 'READ',
});
return response.data;
} catch (error) {
throw new actual.ReportError(
error instanceof Error
? `Failed to mark report as read: ${error.message || 'Unknown error'}`
: 'Failed to mark report as read',
);
}
},
};
});

// Mock auth
vi.mock('@aws-amplify/auth', () => ({
fetchAuthSession: vi.fn().mockResolvedValue({
tokens: {
idToken: {
toString: () => 'mock-id-token',
},
},
}),
}));

// Mock response data
const mockReports = [
{
Expand Down
16 changes: 11 additions & 5 deletions frontend/src/common/api/reportService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,18 @@ export const fetchAllReports = async (): Promise<MedicalReport[]> => {
*/
export const markReportAsRead = async (reportId: string): Promise<MedicalReport> => {
try {
const response = await axios.patch(`${API_URL}/api/reports/${reportId}`, {
status: 'READ',
});
const response = await axios.patch(
`${API_URL}/api/reports/${reportId}`,
{
status: 'READ',
},
await getAuthConfig(),
);

return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new ReportError(`Failed to mark report as read: ${error.message}`);
throw new ReportError(`Failed to mark report as read: ${error.message || 'Unknown error'}`);
}
throw new ReportError('Failed to mark report as read');
}
Expand Down Expand Up @@ -176,7 +180,9 @@ export const toggleReportBookmark = async (
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new ReportError(`Failed to toggle bookmark status: ${error.message}`);
throw new ReportError(
`Failed to toggle bookmark status: ${error.message || 'Unknown error'}`,
);
}
throw new ReportError('Failed to toggle bookmark status');
}
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/common/components/Upload/UploadModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ const UploadModal = ({ isOpen, onClose, onUploadComplete }: UploadModalProps): J
// Automatically redirect to processing screen after 2 seconds
setTimeout(() => {
reset();
onClose();

if (onUploadComplete) {
onUploadComplete(result);
}

// Navigate to the processing tab with reportId in state
if (file) {
history.push('/tabs/processing', {
Expand Down Expand Up @@ -115,6 +116,7 @@ const UploadModal = ({ isOpen, onClose, onUploadComplete }: UploadModalProps): J
// call onUploadComplete now before closing the modal
if (status === UploadStatus.SUCCESS && uploadResult && onUploadComplete) {
onUploadComplete(uploadResult);
return;
}

// Reset state
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/common/hooks/useReports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export const useGetLatestReports = (limit = 3) => {
return useQuery({
queryKey: [LATEST_REPORTS_KEY, limit],
queryFn: () => fetchLatestReports(limit),
refetchOnMount: true,
refetchOnWindowFocus: true,
staleTime: 0, // Consider data immediately stale so it always refreshes
});
};

Expand Down
11 changes: 10 additions & 1 deletion frontend/src/pages/Processing/ProcessingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,21 @@ const ProcessingPage: React.FC = () => {

try {
// Send POST request to backend API
await axios.post(
const response = await axios.post(
`${API_URL}/api/document-processor/process-file`,
{ reportId },
await getAuthConfig(),
);

const data = response.data;

if (data.status === 'processed') {
setIsProcessing(false);

// Redirect to report detail page
history.push(`/tabs/reports/${reportId}`);
}

// Start checking the status every 2 seconds
statusCheckIntervalRef.current = window.setInterval(checkReportStatus, 2000);
} catch (error) {
Expand Down
61 changes: 34 additions & 27 deletions frontend/src/pages/Reports/ReportDetailPage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -239,103 +239,110 @@

&__item {
background-color: #ffffff;
border-radius: 8px;
border-radius: 16px;
padding: 0;
margin-top: 12px;
margin-bottom: 8px;
border-top: 1px solid #ebeef8;
margin: 16px;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
border: 1px solid #ebeef8;
}

&__item-header {
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 12px 16px;
padding: 16px;
background-color: #fff;

&--high {
background-color: rgba(201, 58, 84, 0.08);
border-bottom: 1px solid rgba(201, 58, 84, 0.15);
}

&--low {
background-color: rgba(108, 99, 255, 0.08);
background-color: rgba(250, 173, 113, 0.08);
border-bottom: 1px solid rgba(250, 173, 113, 0.15);
}
}

&__item-name {
font-weight: 500;
font-size: 15px;
font-weight: 600;
font-size: 16px;
margin-right: 12px;
flex: 1;
letter-spacing: 0.26px;
line-height: 15.7px;
line-height: 20px;
color: #313e4c;
}

&__item-level {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
font-size: 13px;
padding: 4px 12px;
border-radius: 12px;
margin-right: 16px;
font-weight: 600;
text-transform: uppercase;
text-transform: capitalize;

&--high {
background-color: rgba(201, 58, 84, 0.1);
color: #c93a54;
background-color: #c93a54;
color: white;
}

&--low {
background-color: rgba(108, 99, 255, 0.1);
color: #435ff0;
background-color: #ffcf99;
color: white;
}
}

&__item-value {
font-weight: 600;
font-size: 15px;
font-size: 16px;
white-space: nowrap;
color: #313e4c;
}

&__item-details {
color: #444;
padding: 10px 16px 16px;
background-color: rgba(235, 238, 248, 0.3);
padding: 16px;
background-color: rgba(248, 249, 251, 0.6);
}

&__item-section {
margin-bottom: 12px;
margin-bottom: 20px;

&:last-child {
margin-bottom: 0;
}

h4 {
font-size: 14px;
font-size: 15px;
color: #313e4c;
margin: 0 0 4px 0;
margin: 0 0 8px 0;
font-weight: 600;
}

p {
margin: 0;
font-size: 14px;
line-height: 1.4;
line-height: 1.5;
color: #5c6d80;
}
}

&__item-list {
margin: 0;
padding-left: 18px;
font-size: 13px;
padding-left: 20px;
font-size: 14px;
line-height: 1.5;
color: #5c6d80;

li {
margin-bottom: 6px;
margin-bottom: 10px;
padding-left: 4px;

&:last-child {
margin-bottom: 0;
}
}
}

Expand Down
Loading