ai prediction data mapped to the new tab called ai prediction
This commit is contained in:
parent
84b63e401f
commit
e8492cd442
249
app/modules/AIPrediction/__tests__/AIPredictionCard.test.tsx
Normal file
249
app/modules/AIPrediction/__tests__/AIPredictionCard.test.tsx
Normal file
@ -0,0 +1,249 @@
|
||||
/*
|
||||
* File: AIPredictionCard.test.tsx
|
||||
* Description: Unit tests for AI Prediction Card component
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react-native';
|
||||
import AIPredictionCard from '../components/AIPredictionCard';
|
||||
import type { AIPredictionCase } from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// MOCK DATA
|
||||
// ============================================================================
|
||||
|
||||
const mockPredictionCase: AIPredictionCase = {
|
||||
patid: 'test-patient-001',
|
||||
hospital_id: 'hospital-123',
|
||||
prediction: {
|
||||
label: 'midline shift',
|
||||
finding_type: 'pathology',
|
||||
clinical_urgency: 'urgent',
|
||||
confidence_score: 0.96,
|
||||
finding_category: 'abnormal',
|
||||
primary_severity: 'high',
|
||||
anatomical_location: 'brain',
|
||||
},
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
updated_at: '2024-01-15T10:30:00Z',
|
||||
review_status: 'pending',
|
||||
priority: 'critical',
|
||||
};
|
||||
|
||||
const mockProps = {
|
||||
predictionCase: mockPredictionCase,
|
||||
onPress: jest.fn(),
|
||||
onReview: jest.fn(),
|
||||
onToggleSelect: jest.fn(),
|
||||
isSelected: false,
|
||||
showReviewButton: true,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// UNIT TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('AIPredictionCard', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// RENDERING TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render correctly with required props', () => {
|
||||
const { getByText } = render(
|
||||
<AIPredictionCard
|
||||
predictionCase={mockPredictionCase}
|
||||
onPress={mockProps.onPress}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByText('test-patient-001')).toBeTruthy();
|
||||
expect(getByText('Midline Shift')).toBeTruthy();
|
||||
expect(getByText('96%')).toBeTruthy();
|
||||
expect(getByText('Urgent')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render review button when showReviewButton is true', () => {
|
||||
const { getByText } = render(<AIPredictionCard {...mockProps} />);
|
||||
expect(getByText('Review')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not render review button when showReviewButton is false', () => {
|
||||
const { queryByText } = render(
|
||||
<AIPredictionCard {...mockProps} showReviewButton={false} />
|
||||
);
|
||||
expect(queryByText('Review')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render review button when status is not pending', () => {
|
||||
const reviewedCase = {
|
||||
...mockPredictionCase,
|
||||
review_status: 'reviewed' as const,
|
||||
};
|
||||
|
||||
const { queryByText } = render(
|
||||
<AIPredictionCard
|
||||
{...mockProps}
|
||||
predictionCase={reviewedCase}
|
||||
/>
|
||||
);
|
||||
expect(queryByText('Review')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render selection checkbox when onToggleSelect is provided', () => {
|
||||
const { getByRole } = render(<AIPredictionCard {...mockProps} />);
|
||||
expect(getByRole('checkbox')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show selected state correctly', () => {
|
||||
const { getByRole } = render(
|
||||
<AIPredictionCard {...mockProps} isSelected={true} />
|
||||
);
|
||||
|
||||
const checkbox = getByRole('checkbox');
|
||||
expect(checkbox.props.accessibilityState.checked).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// INTERACTION TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Interactions', () => {
|
||||
it('should call onPress when card is pressed', () => {
|
||||
const { getByRole } = render(<AIPredictionCard {...mockProps} />);
|
||||
|
||||
fireEvent.press(getByRole('button'));
|
||||
expect(mockProps.onPress).toHaveBeenCalledWith(mockPredictionCase);
|
||||
});
|
||||
|
||||
it('should call onReview when review button is pressed', () => {
|
||||
const { getByText } = render(<AIPredictionCard {...mockProps} />);
|
||||
|
||||
fireEvent.press(getByText('Review'));
|
||||
expect(mockProps.onReview).toHaveBeenCalledWith('test-patient-001');
|
||||
});
|
||||
|
||||
it('should call onToggleSelect when checkbox is pressed', () => {
|
||||
const { getByRole } = render(<AIPredictionCard {...mockProps} />);
|
||||
|
||||
fireEvent.press(getByRole('checkbox'));
|
||||
expect(mockProps.onToggleSelect).toHaveBeenCalledWith('test-patient-001');
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// DATA FORMATTING TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Data formatting', () => {
|
||||
it('should format confidence score as percentage', () => {
|
||||
const { getByText } = render(<AIPredictionCard {...mockProps} />);
|
||||
expect(getByText('96%')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should capitalize text correctly', () => {
|
||||
const { getByText } = render(<AIPredictionCard {...mockProps} />);
|
||||
expect(getByText('Midline Shift')).toBeTruthy();
|
||||
expect(getByText('Pathology')).toBeTruthy();
|
||||
expect(getByText('Abnormal')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle missing anatomical location', () => {
|
||||
const caseWithoutLocation = {
|
||||
...mockPredictionCase,
|
||||
prediction: {
|
||||
...mockPredictionCase.prediction,
|
||||
anatomical_location: 'not_applicable',
|
||||
},
|
||||
};
|
||||
|
||||
const { queryByText } = render(
|
||||
<AIPredictionCard
|
||||
{...mockProps}
|
||||
predictionCase={caseWithoutLocation}
|
||||
/>
|
||||
);
|
||||
|
||||
// Should not render location when it's 'not_applicable'
|
||||
expect(queryByText('Not Applicable')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// ACCESSIBILITY TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Accessibility', () => {
|
||||
it('should have proper accessibility labels', () => {
|
||||
const { getByLabelText } = render(<AIPredictionCard {...mockProps} />);
|
||||
|
||||
expect(
|
||||
getByLabelText('AI Prediction case for patient test-patient-001')
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have proper accessibility hints', () => {
|
||||
const { getByRole } = render(<AIPredictionCard {...mockProps} />);
|
||||
|
||||
const cardButton = getByRole('button');
|
||||
expect(cardButton.props.accessibilityHint).toBe(
|
||||
'Tap to view detailed prediction information'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// EDGE CASES TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle missing dates gracefully', () => {
|
||||
const caseWithoutDates = {
|
||||
...mockPredictionCase,
|
||||
created_at: undefined,
|
||||
updated_at: undefined,
|
||||
};
|
||||
|
||||
const { getByText } = render(
|
||||
<AIPredictionCard
|
||||
{...mockProps}
|
||||
predictionCase={caseWithoutDates}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByText('N/A')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle emergency urgency with special styling', () => {
|
||||
const emergencyCase = {
|
||||
...mockPredictionCase,
|
||||
prediction: {
|
||||
...mockPredictionCase.prediction,
|
||||
clinical_urgency: 'emergency' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const { getByText } = render(
|
||||
<AIPredictionCard
|
||||
{...mockProps}
|
||||
predictionCase={emergencyCase}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(getByText('Emergency')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* End of File: AIPredictionCard.test.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
361
app/modules/AIPrediction/__tests__/aiPredictionAPI.test.ts
Normal file
361
app/modules/AIPrediction/__tests__/aiPredictionAPI.test.ts
Normal file
@ -0,0 +1,361 @@
|
||||
/*
|
||||
* File: aiPredictionAPI.test.ts
|
||||
* Description: Unit tests for AI Prediction API service
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import { aiPredictionAPI } from '../services/aiPredictionAPI';
|
||||
|
||||
// Mock apisauce
|
||||
jest.mock('apisauce', () => ({
|
||||
create: jest.fn(() => ({
|
||||
get: jest.fn(),
|
||||
post: jest.fn(),
|
||||
put: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock API utilities
|
||||
jest.mock('../../../shared/utils', () => ({
|
||||
API_CONFIG: {
|
||||
BASE_URL: 'https://test-api.com',
|
||||
},
|
||||
buildHeaders: jest.fn((options = {}) => ({
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.token && { Authorization: `Bearer ${options.token}` }),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
// ============================================================================
|
||||
// MOCK DATA
|
||||
// ============================================================================
|
||||
|
||||
const mockToken = 'test-token-123';
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
data: {
|
||||
success: true,
|
||||
data: [
|
||||
{
|
||||
patid: 'test-001',
|
||||
hospital_id: 'hospital-001',
|
||||
prediction: {
|
||||
label: 'test finding',
|
||||
finding_type: 'pathology',
|
||||
clinical_urgency: 'urgent',
|
||||
confidence_score: 0.95,
|
||||
finding_category: 'abnormal',
|
||||
primary_severity: 'high',
|
||||
anatomical_location: 'brain',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// UNIT TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('AI Prediction API', () => {
|
||||
let mockApi: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Get the mocked API instance
|
||||
const { create } = require('apisauce');
|
||||
mockApi = create();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// GET ALL PREDICTIONS TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('getAllPredictions', () => {
|
||||
it('should call GET endpoint with correct parameters', async () => {
|
||||
mockApi.get.mockResolvedValue(mockResponse);
|
||||
|
||||
const params = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
urgency: 'urgent',
|
||||
search: 'test',
|
||||
};
|
||||
|
||||
await aiPredictionAPI.getAllPredictions(mockToken, params);
|
||||
|
||||
expect(mockApi.get).toHaveBeenCalledWith(
|
||||
'/api/ai-cases/all-prediction-results',
|
||||
params,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call GET endpoint without parameters', async () => {
|
||||
mockApi.get.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.getAllPredictions(mockToken);
|
||||
|
||||
expect(mockApi.get).toHaveBeenCalledWith(
|
||||
'/api/ai-cases/all-prediction-results',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// GET CASE DETAILS TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('getCaseDetails', () => {
|
||||
it('should call GET endpoint with correct case ID', async () => {
|
||||
const caseId = 'test-case-001';
|
||||
mockApi.get.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.getCaseDetails(caseId, mockToken);
|
||||
|
||||
expect(mockApi.get).toHaveBeenCalledWith(
|
||||
`/api/ai-cases/prediction-details/${caseId}`,
|
||||
{},
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// UPDATE CASE REVIEW TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('updateCaseReview', () => {
|
||||
it('should call PUT endpoint with correct data', async () => {
|
||||
const caseId = 'test-case-001';
|
||||
const reviewData = {
|
||||
review_status: 'reviewed' as const,
|
||||
reviewed_by: 'Dr. Test',
|
||||
review_notes: 'Test notes',
|
||||
priority: 'high' as const,
|
||||
};
|
||||
|
||||
mockApi.put.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.updateCaseReview(caseId, reviewData, mockToken);
|
||||
|
||||
expect(mockApi.put).toHaveBeenCalledWith(
|
||||
`/api/ai-cases/review/${caseId}`,
|
||||
reviewData,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// GET STATISTICS TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('getPredictionStats', () => {
|
||||
it('should call GET endpoint with time range parameter', async () => {
|
||||
mockApi.get.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.getPredictionStats(mockToken, 'week');
|
||||
|
||||
expect(mockApi.get).toHaveBeenCalledWith(
|
||||
'/api/ai-cases/statistics',
|
||||
{ timeRange: 'week' },
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call GET endpoint without time range parameter', async () => {
|
||||
mockApi.get.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.getPredictionStats(mockToken);
|
||||
|
||||
expect(mockApi.get).toHaveBeenCalledWith(
|
||||
'/api/ai-cases/statistics',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// SEARCH PREDICTIONS TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('searchPredictions', () => {
|
||||
it('should call GET endpoint with search query and filters', async () => {
|
||||
const query = 'test search';
|
||||
const filters = {
|
||||
urgency: ['urgent', 'emergency'],
|
||||
severity: ['high'],
|
||||
dateRange: { start: '2024-01-01', end: '2024-01-31' },
|
||||
};
|
||||
|
||||
mockApi.get.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.searchPredictions(query, mockToken, filters);
|
||||
|
||||
expect(mockApi.get).toHaveBeenCalledWith(
|
||||
'/api/ai-cases/search',
|
||||
{
|
||||
q: query,
|
||||
filters: JSON.stringify(filters),
|
||||
},
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should call GET endpoint with only search query', async () => {
|
||||
const query = 'test search';
|
||||
mockApi.get.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.searchPredictions(query, mockToken);
|
||||
|
||||
expect(mockApi.get).toHaveBeenCalledWith(
|
||||
'/api/ai-cases/search',
|
||||
{ q: query },
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// BULK OPERATIONS TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('bulkUpdateReviews', () => {
|
||||
it('should call PUT endpoint with case IDs and review data', async () => {
|
||||
const caseIds = ['case-001', 'case-002', 'case-003'];
|
||||
const reviewData = {
|
||||
review_status: 'reviewed' as const,
|
||||
reviewed_by: 'Dr. Test',
|
||||
review_notes: 'Bulk review',
|
||||
};
|
||||
|
||||
mockApi.put.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.bulkUpdateReviews(caseIds, reviewData, mockToken);
|
||||
|
||||
expect(mockApi.put).toHaveBeenCalledWith(
|
||||
'/api/ai-cases/bulk-review',
|
||||
{ caseIds, reviewData },
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// SUBMIT FEEDBACK TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('submitPredictionFeedback', () => {
|
||||
it('should call POST endpoint with feedback data', async () => {
|
||||
const caseId = 'test-case-001';
|
||||
const feedbackData = {
|
||||
accuracy_rating: 4 as const,
|
||||
is_accurate: true,
|
||||
physician_diagnosis: 'Confirmed midline shift',
|
||||
feedback_notes: 'Accurate prediction',
|
||||
improvement_suggestions: 'None',
|
||||
};
|
||||
|
||||
mockApi.post.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.submitPredictionFeedback(caseId, feedbackData, mockToken);
|
||||
|
||||
expect(mockApi.post).toHaveBeenCalledWith(
|
||||
`/api/ai-cases/feedback/${caseId}`,
|
||||
feedbackData,
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: `Bearer ${mockToken}`,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// ERROR HANDLING TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Error handling', () => {
|
||||
it('should handle API errors gracefully', async () => {
|
||||
const errorResponse = {
|
||||
ok: false,
|
||||
problem: 'NETWORK_ERROR',
|
||||
data: null,
|
||||
};
|
||||
|
||||
mockApi.get.mockResolvedValue(errorResponse);
|
||||
|
||||
const result = await aiPredictionAPI.getAllPredictions(mockToken);
|
||||
expect(result).toEqual(errorResponse);
|
||||
});
|
||||
|
||||
it('should handle missing token', async () => {
|
||||
mockApi.get.mockResolvedValue(mockResponse);
|
||||
|
||||
await aiPredictionAPI.getAllPredictions('');
|
||||
|
||||
expect(mockApi.get).toHaveBeenCalledWith(
|
||||
'/api/ai-cases/all-prediction-results',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* End of File: aiPredictionAPI.test.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
231
app/modules/AIPrediction/__tests__/aiPredictionSlice.test.ts
Normal file
231
app/modules/AIPrediction/__tests__/aiPredictionSlice.test.ts
Normal file
@ -0,0 +1,231 @@
|
||||
/*
|
||||
* File: aiPredictionSlice.test.ts
|
||||
* Description: Unit tests for AI Prediction Redux slice
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import aiPredictionReducer, {
|
||||
setSearchQuery,
|
||||
setUrgencyFilter,
|
||||
setSeverityFilter,
|
||||
setCategoryFilter,
|
||||
clearAllFilters,
|
||||
toggleShowFilters,
|
||||
clearError,
|
||||
} from '../redux/aiPredictionSlice';
|
||||
import type { AIPredictionState } from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// MOCK DATA
|
||||
// ============================================================================
|
||||
|
||||
const initialState: AIPredictionState = {
|
||||
predictionCases: [],
|
||||
currentCase: null,
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
isLoadingCaseDetails: false,
|
||||
error: null,
|
||||
searchQuery: '',
|
||||
selectedUrgencyFilter: 'all',
|
||||
selectedSeverityFilter: 'all',
|
||||
selectedCategoryFilter: 'all',
|
||||
sortBy: 'date',
|
||||
sortOrder: 'desc',
|
||||
currentPage: 1,
|
||||
itemsPerPage: 20,
|
||||
totalItems: 0,
|
||||
lastUpdated: null,
|
||||
cacheExpiry: null,
|
||||
showFilters: false,
|
||||
selectedCaseIds: [],
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// UNIT TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('AI Prediction Slice', () => {
|
||||
// ============================================================================
|
||||
// INITIAL STATE TESTS
|
||||
// ============================================================================
|
||||
|
||||
it('should return the initial state', () => {
|
||||
const result = aiPredictionReducer(undefined, { type: 'unknown' });
|
||||
expect(result).toEqual(initialState);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// SEARCH TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Search functionality', () => {
|
||||
it('should handle setSearchQuery', () => {
|
||||
const searchQuery = 'test search';
|
||||
const action = setSearchQuery(searchQuery);
|
||||
const result = aiPredictionReducer(initialState, action);
|
||||
|
||||
expect(result.searchQuery).toBe(searchQuery);
|
||||
expect(result.currentPage).toBe(1); // Should reset to first page
|
||||
});
|
||||
|
||||
it('should handle empty search query', () => {
|
||||
const state = { ...initialState, searchQuery: 'existing search' };
|
||||
const action = setSearchQuery('');
|
||||
const result = aiPredictionReducer(state, action);
|
||||
|
||||
expect(result.searchQuery).toBe('');
|
||||
expect(result.currentPage).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// FILTER TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Filter functionality', () => {
|
||||
it('should handle setUrgencyFilter', () => {
|
||||
const filter = 'urgent';
|
||||
const action = setUrgencyFilter(filter);
|
||||
const result = aiPredictionReducer(initialState, action);
|
||||
|
||||
expect(result.selectedUrgencyFilter).toBe(filter);
|
||||
expect(result.currentPage).toBe(1); // Should reset to first page
|
||||
});
|
||||
|
||||
it('should handle setSeverityFilter', () => {
|
||||
const filter = 'high';
|
||||
const action = setSeverityFilter(filter);
|
||||
const result = aiPredictionReducer(initialState, action);
|
||||
|
||||
expect(result.selectedSeverityFilter).toBe(filter);
|
||||
expect(result.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle setCategoryFilter', () => {
|
||||
const filter = 'critical';
|
||||
const action = setCategoryFilter(filter);
|
||||
const result = aiPredictionReducer(initialState, action);
|
||||
|
||||
expect(result.selectedCategoryFilter).toBe(filter);
|
||||
expect(result.currentPage).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle clearAllFilters', () => {
|
||||
const state: AIPredictionState = {
|
||||
...initialState,
|
||||
searchQuery: 'test',
|
||||
selectedUrgencyFilter: 'urgent',
|
||||
selectedSeverityFilter: 'high',
|
||||
selectedCategoryFilter: 'critical',
|
||||
currentPage: 3,
|
||||
};
|
||||
|
||||
const action = clearAllFilters();
|
||||
const result = aiPredictionReducer(state, action);
|
||||
|
||||
expect(result.searchQuery).toBe('');
|
||||
expect(result.selectedUrgencyFilter).toBe('all');
|
||||
expect(result.selectedSeverityFilter).toBe('all');
|
||||
expect(result.selectedCategoryFilter).toBe('all');
|
||||
expect(result.currentPage).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// UI STATE TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('UI state functionality', () => {
|
||||
it('should handle toggleShowFilters', () => {
|
||||
const action = toggleShowFilters();
|
||||
|
||||
// Toggle from false to true
|
||||
const result1 = aiPredictionReducer(initialState, action);
|
||||
expect(result1.showFilters).toBe(true);
|
||||
|
||||
// Toggle from true to false
|
||||
const result2 = aiPredictionReducer(result1, action);
|
||||
expect(result2.showFilters).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle clearError', () => {
|
||||
const state = { ...initialState, error: 'Test error' };
|
||||
const action = clearError();
|
||||
const result = aiPredictionReducer(state, action);
|
||||
|
||||
expect(result.error).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// ASYNC ACTION TESTS
|
||||
// ============================================================================
|
||||
|
||||
describe('Async actions', () => {
|
||||
it('should handle fetchAIPredictions.pending', () => {
|
||||
const action = { type: 'aiPrediction/fetchAIPredictions/pending' };
|
||||
const result = aiPredictionReducer(initialState, action);
|
||||
|
||||
expect(result.isLoading).toBe(true);
|
||||
expect(result.error).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle fetchAIPredictions.fulfilled', () => {
|
||||
const mockCases = [
|
||||
{
|
||||
patid: 'test-001',
|
||||
hospital_id: 'hospital-001',
|
||||
prediction: {
|
||||
label: 'test finding',
|
||||
finding_type: 'pathology' as const,
|
||||
clinical_urgency: 'urgent' as const,
|
||||
confidence_score: 0.95,
|
||||
finding_category: 'abnormal' as const,
|
||||
primary_severity: 'high' as const,
|
||||
anatomical_location: 'brain',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const action = {
|
||||
type: 'aiPrediction/fetchAIPredictions/fulfilled',
|
||||
payload: {
|
||||
cases: mockCases,
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 20,
|
||||
},
|
||||
};
|
||||
|
||||
const result = aiPredictionReducer(initialState, action);
|
||||
|
||||
expect(result.isLoading).toBe(false);
|
||||
expect(result.predictionCases).toEqual(mockCases);
|
||||
expect(result.totalItems).toBe(1);
|
||||
expect(result.error).toBe(null);
|
||||
expect(result.lastUpdated).toBeInstanceOf(Date);
|
||||
expect(result.cacheExpiry).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it('should handle fetchAIPredictions.rejected', () => {
|
||||
const errorMessage = 'Failed to fetch predictions';
|
||||
const action = {
|
||||
type: 'aiPrediction/fetchAIPredictions/rejected',
|
||||
payload: errorMessage,
|
||||
};
|
||||
|
||||
const result = aiPredictionReducer(initialState, action);
|
||||
|
||||
expect(result.isLoading).toBe(false);
|
||||
expect(result.error).toBe(errorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* End of File: aiPredictionSlice.test.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
522
app/modules/AIPrediction/components/AIPredictionCard.tsx
Normal file
522
app/modules/AIPrediction/components/AIPredictionCard.tsx
Normal file
@ -0,0 +1,522 @@
|
||||
/*
|
||||
* File: AIPredictionCard.tsx
|
||||
* Description: Card component for displaying AI prediction case information
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme';
|
||||
import { AIPredictionCase, URGENCY_COLORS, SEVERITY_COLORS, CATEGORY_COLORS } from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface AIPredictionCardProps {
|
||||
predictionCase: AIPredictionCase;
|
||||
onPress: (predictionCase: AIPredictionCase) => void;
|
||||
onReview?: (caseId: string) => void;
|
||||
isSelected?: boolean;
|
||||
onToggleSelect?: (caseId: string) => void;
|
||||
showReviewButton?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const CARD_WIDTH = width - 32; // Full width with margins
|
||||
|
||||
// ============================================================================
|
||||
// AI PREDICTION CARD COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AIPredictionCard Component
|
||||
*
|
||||
* Purpose: Display AI prediction case information in a card format
|
||||
*
|
||||
* Features:
|
||||
* - Patient ID and hospital information
|
||||
* - AI prediction results with confidence score
|
||||
* - Urgency and severity indicators
|
||||
* - Review status and actions
|
||||
* - Selection support for bulk operations
|
||||
* - Modern card design with proper spacing
|
||||
* - Color-coded priority indicators
|
||||
* - Accessibility support
|
||||
*/
|
||||
const AIPredictionCard: React.FC<AIPredictionCardProps> = ({
|
||||
predictionCase,
|
||||
onPress,
|
||||
onReview,
|
||||
isSelected = false,
|
||||
onToggleSelect,
|
||||
showReviewButton = true,
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get Urgency Color
|
||||
*
|
||||
* Purpose: Get color based on clinical urgency
|
||||
*/
|
||||
const getUrgencyColor = (urgency: string): string => {
|
||||
return URGENCY_COLORS[urgency as keyof typeof URGENCY_COLORS] || theme.colors.textMuted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Severity Color
|
||||
*
|
||||
* Purpose: Get color based on primary severity
|
||||
*/
|
||||
const getSeverityColor = (severity: string): string => {
|
||||
return SEVERITY_COLORS[severity as keyof typeof SEVERITY_COLORS] || theme.colors.textMuted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Category Color
|
||||
*
|
||||
* Purpose: Get color based on finding category
|
||||
*/
|
||||
const getCategoryColor = (category: string): string => {
|
||||
return CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS] || theme.colors.textMuted;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Review Status Color
|
||||
*
|
||||
* Purpose: Get color based on review status
|
||||
*/
|
||||
const getReviewStatusColor = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'confirmed':
|
||||
return theme.colors.success;
|
||||
case 'reviewed':
|
||||
return theme.colors.info;
|
||||
case 'disputed':
|
||||
return theme.colors.warning;
|
||||
case 'pending':
|
||||
default:
|
||||
return theme.colors.error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Format Confidence Score
|
||||
*
|
||||
* Purpose: Format confidence score as percentage
|
||||
*/
|
||||
const formatConfidence = (score: number): string => {
|
||||
return `${Math.round(score * 100)}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Capitalize Text
|
||||
*
|
||||
* Purpose: Capitalize first letter of each word
|
||||
*/
|
||||
const capitalize = (text: string): string => {
|
||||
return text.split('_').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* Format Date
|
||||
*
|
||||
* Purpose: Format date for display
|
||||
*/
|
||||
const formatDate = (dateString?: string): string => {
|
||||
if (!dateString) return 'N/A';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return 'N/A';
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle Card Press
|
||||
*
|
||||
* Purpose: Handle card tap to view details
|
||||
*/
|
||||
const handleCardPress = () => {
|
||||
onPress(predictionCase);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle Review Press
|
||||
*
|
||||
* Purpose: Handle review button press
|
||||
*/
|
||||
const handleReviewPress = (event: any) => {
|
||||
event.stopPropagation();
|
||||
if (onReview) {
|
||||
onReview(predictionCase.patid);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle Selection Toggle
|
||||
*
|
||||
* Purpose: Handle case selection toggle
|
||||
*/
|
||||
const handleSelectionToggle = (event: any) => {
|
||||
event.stopPropagation();
|
||||
if (onToggleSelect) {
|
||||
onToggleSelect(predictionCase.patid);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.container,
|
||||
isSelected && styles.selectedContainer,
|
||||
predictionCase.prediction.clinical_urgency === 'emergency' && styles.emergencyContainer,
|
||||
]}
|
||||
onPress={handleCardPress}
|
||||
activeOpacity={0.7}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`AI Prediction case for patient ${predictionCase.patid}`}
|
||||
accessibilityHint="Tap to view detailed prediction information"
|
||||
>
|
||||
{/* Header Section */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<Text style={styles.patientId} numberOfLines={1}>
|
||||
{predictionCase.patid}
|
||||
</Text>
|
||||
<Text style={styles.date}>
|
||||
{formatDate(predictionCase.processed_at)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerRight}>
|
||||
{onToggleSelect && (
|
||||
<TouchableOpacity
|
||||
style={styles.selectionButton}
|
||||
onPress={handleSelectionToggle}
|
||||
accessibilityRole="checkbox"
|
||||
accessibilityState={{ checked: isSelected }}
|
||||
>
|
||||
<Icon
|
||||
name={isSelected ? 'check-square' : 'square'}
|
||||
size={20}
|
||||
color={isSelected ? theme.colors.primary : theme.colors.textMuted}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<View style={[
|
||||
styles.priorityBadge,
|
||||
{ backgroundColor: getUrgencyColor(predictionCase.prediction.clinical_urgency) }
|
||||
]}>
|
||||
<Text style={styles.priorityText}>
|
||||
{capitalize(predictionCase.prediction.clinical_urgency)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Prediction Information */}
|
||||
<View style={styles.predictionSection}>
|
||||
<View style={styles.predictionHeader}>
|
||||
<Text style={styles.predictionLabel} numberOfLines={2}>
|
||||
{capitalize(predictionCase.prediction.label)}
|
||||
</Text>
|
||||
<View style={styles.confidenceContainer}>
|
||||
<Icon name="trending-up" size={16} color={theme.colors.primary} />
|
||||
<Text style={styles.confidenceText}>
|
||||
{formatConfidence(predictionCase.prediction.confidence_score)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Finding Details */}
|
||||
<View style={styles.findingDetails}>
|
||||
<View style={styles.findingItem}>
|
||||
<Text style={styles.findingLabel}>Type:</Text>
|
||||
<Text style={styles.findingValue}>
|
||||
{capitalize(predictionCase.prediction.finding_type)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.findingItem}>
|
||||
<Text style={styles.findingLabel}>Category:</Text>
|
||||
<View style={[
|
||||
styles.categoryBadge,
|
||||
{ backgroundColor: getCategoryColor(predictionCase.prediction.finding_category) }
|
||||
]}>
|
||||
<Text style={styles.categoryText}>
|
||||
{capitalize(predictionCase.prediction.finding_category)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Severity and Location */}
|
||||
<View style={styles.detailsRow}>
|
||||
<View style={styles.detailItem}>
|
||||
<Icon name="alert-triangle" size={14} color={getSeverityColor(predictionCase.prediction.primary_severity)} />
|
||||
<Text style={[styles.detailText, { color: getSeverityColor(predictionCase.prediction.primary_severity) }]}>
|
||||
{capitalize(predictionCase.prediction.primary_severity)} Severity
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{predictionCase.prediction.anatomical_location !== 'not_applicable' && (
|
||||
<View style={styles.detailItem}>
|
||||
<Icon name="map-pin" size={14} color={theme.colors.textSecondary} />
|
||||
<Text style={styles.detailText}>
|
||||
{capitalize(predictionCase.prediction.anatomical_location)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Footer Section */}
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.footerLeft}>
|
||||
<View style={[
|
||||
styles.reviewStatusBadge,
|
||||
{ backgroundColor: getReviewStatusColor(predictionCase.review_status || 'pending') }
|
||||
]}>
|
||||
<Text style={styles.reviewStatusText}>
|
||||
{capitalize(predictionCase.review_status || 'pending')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{predictionCase.reviewed_by && (
|
||||
<Text style={styles.reviewedBy}>
|
||||
by {predictionCase.reviewed_by}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{showReviewButton && predictionCase.review_status === 'pending' && (
|
||||
<TouchableOpacity
|
||||
style={styles.reviewButton}
|
||||
onPress={handleReviewPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Review this case"
|
||||
>
|
||||
<Icon name="eye" size={16} color={theme.colors.primary} />
|
||||
<Text style={styles.reviewButtonText}>Review</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: theme.colors.background,
|
||||
borderRadius: theme.borderRadius.large,
|
||||
padding: theme.spacing.lg,
|
||||
marginHorizontal: theme.spacing.md,
|
||||
marginVertical: theme.spacing.sm,
|
||||
width: CARD_WIDTH,
|
||||
...theme.shadows.medium,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
selectedContainer: {
|
||||
borderColor: theme.colors.primary,
|
||||
borderWidth: 2,
|
||||
},
|
||||
emergencyContainer: {
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: URGENCY_COLORS.emergency,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
headerLeft: {
|
||||
flex: 1,
|
||||
},
|
||||
headerRight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
patientId: {
|
||||
fontSize: theme.typography.fontSize.bodyLarge,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
marginBottom: theme.spacing.xs,
|
||||
},
|
||||
date: {
|
||||
fontSize: theme.typography.fontSize.bodySmall,
|
||||
color: theme.colors.textSecondary,
|
||||
},
|
||||
selectionButton: {
|
||||
padding: theme.spacing.xs,
|
||||
},
|
||||
priorityBadge: {
|
||||
paddingHorizontal: theme.spacing.sm,
|
||||
paddingVertical: theme.spacing.xs,
|
||||
borderRadius: theme.borderRadius.small,
|
||||
},
|
||||
priorityText: {
|
||||
fontSize: theme.typography.fontSize.caption,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.background,
|
||||
},
|
||||
predictionSection: {
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
predictionHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
predictionLabel: {
|
||||
fontSize: theme.typography.fontSize.bodyLarge,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.textPrimary,
|
||||
flex: 1,
|
||||
marginRight: theme.spacing.sm,
|
||||
},
|
||||
confidenceContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.xs,
|
||||
},
|
||||
confidenceText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
findingDetails: {
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
findingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: theme.spacing.xs,
|
||||
},
|
||||
findingLabel: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textSecondary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
findingValue: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
categoryBadge: {
|
||||
paddingHorizontal: theme.spacing.sm,
|
||||
paddingVertical: theme.spacing.xs,
|
||||
borderRadius: theme.borderRadius.small,
|
||||
},
|
||||
categoryText: {
|
||||
fontSize: theme.typography.fontSize.caption,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.background,
|
||||
},
|
||||
detailsRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
detailItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.xs,
|
||||
flex: 1,
|
||||
},
|
||||
detailText: {
|
||||
fontSize: theme.typography.fontSize.bodySmall,
|
||||
color: theme.colors.textSecondary,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: theme.spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: theme.colors.border,
|
||||
},
|
||||
footerLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.sm,
|
||||
flex: 1,
|
||||
},
|
||||
reviewStatusBadge: {
|
||||
paddingHorizontal: theme.spacing.sm,
|
||||
paddingVertical: theme.spacing.xs,
|
||||
borderRadius: theme.borderRadius.small,
|
||||
},
|
||||
reviewStatusText: {
|
||||
fontSize: theme.typography.fontSize.caption,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.background,
|
||||
},
|
||||
reviewedBy: {
|
||||
fontSize: theme.typography.fontSize.caption,
|
||||
color: theme.colors.textMuted,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
reviewButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.xs,
|
||||
paddingHorizontal: theme.spacing.sm,
|
||||
paddingVertical: theme.spacing.xs,
|
||||
borderRadius: theme.borderRadius.small,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.primary,
|
||||
},
|
||||
reviewButtonText: {
|
||||
fontSize: theme.typography.fontSize.bodySmall,
|
||||
color: theme.colors.primary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
});
|
||||
|
||||
export default AIPredictionCard;
|
||||
|
||||
/*
|
||||
* End of File: AIPredictionCard.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
287
app/modules/AIPrediction/components/EmptyState.tsx
Normal file
287
app/modules/AIPrediction/components/EmptyState.tsx
Normal file
@ -0,0 +1,287 @@
|
||||
/*
|
||||
* File: EmptyState.tsx
|
||||
* Description: Empty state component for AI predictions
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface EmptyStateProps {
|
||||
title?: string;
|
||||
message?: string;
|
||||
iconName?: string;
|
||||
actionText?: string;
|
||||
onAction?: () => void;
|
||||
style?: any;
|
||||
showRefreshButton?: boolean;
|
||||
onRefresh?: () => void;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
// ============================================================================
|
||||
// EMPTY STATE COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* EmptyState Component
|
||||
*
|
||||
* Purpose: Display empty state for AI predictions
|
||||
*
|
||||
* Features:
|
||||
* - Customizable title and message
|
||||
* - Icon display with customizable icon
|
||||
* - Optional action button
|
||||
* - Refresh functionality
|
||||
* - Responsive design
|
||||
* - Modern empty state design
|
||||
* - Accessibility support
|
||||
*/
|
||||
const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
title = 'No AI Predictions Found',
|
||||
message = 'There are no AI prediction cases available at the moment. Try adjusting your filters or refresh to see new predictions.',
|
||||
iconName = 'brain',
|
||||
actionText = 'Refresh',
|
||||
onAction,
|
||||
style,
|
||||
showRefreshButton = true,
|
||||
onRefresh,
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle Action Press
|
||||
*
|
||||
* Purpose: Handle action button press
|
||||
*/
|
||||
const handleActionPress = () => {
|
||||
if (onAction) {
|
||||
onAction();
|
||||
} else if (onRefresh) {
|
||||
onRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{/* Empty State Icon */}
|
||||
<View style={styles.iconContainer}>
|
||||
<Icon
|
||||
name={iconName}
|
||||
size={64}
|
||||
color={theme.colors.textMuted}
|
||||
style={styles.icon}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Empty State Title */}
|
||||
<Text style={styles.title} accessibilityRole="header">
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{/* Empty State Message */}
|
||||
<Text style={styles.message}>
|
||||
{message}
|
||||
</Text>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.buttonsContainer}>
|
||||
{/* Primary Action Button */}
|
||||
{(onAction || onRefresh) && (
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={handleActionPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={actionText}
|
||||
>
|
||||
<Icon
|
||||
name="refresh-cw"
|
||||
size={18}
|
||||
color={theme.colors.background}
|
||||
style={styles.buttonIcon}
|
||||
/>
|
||||
<Text style={styles.actionButtonText}>
|
||||
{actionText}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Secondary Refresh Button */}
|
||||
{showRefreshButton && onRefresh && !onAction && (
|
||||
<TouchableOpacity
|
||||
style={styles.secondaryButton}
|
||||
onPress={onRefresh}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Refresh data"
|
||||
>
|
||||
<Icon
|
||||
name="refresh-cw"
|
||||
size={16}
|
||||
color={theme.colors.primary}
|
||||
style={styles.buttonIcon}
|
||||
/>
|
||||
<Text style={styles.secondaryButtonText}>
|
||||
Refresh Data
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Suggestions */}
|
||||
<View style={styles.suggestionsContainer}>
|
||||
<Text style={styles.suggestionsTitle}>Try:</Text>
|
||||
<View style={styles.suggestionsList}>
|
||||
<View style={styles.suggestionItem}>
|
||||
<Icon name="search" size={14} color={theme.colors.textMuted} />
|
||||
<Text style={styles.suggestionText}>Clearing search filters</Text>
|
||||
</View>
|
||||
<View style={styles.suggestionItem}>
|
||||
<Icon name="filter" size={14} color={theme.colors.textMuted} />
|
||||
<Text style={styles.suggestionText}>Adjusting filter criteria</Text>
|
||||
</View>
|
||||
<View style={styles.suggestionItem}>
|
||||
<Icon name="refresh-cw" size={14} color={theme.colors.textMuted} />
|
||||
<Text style={styles.suggestionText}>Refreshing the data</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.xl,
|
||||
paddingVertical: theme.spacing.xxl,
|
||||
minHeight: height * 0.4,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 60,
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing.xl,
|
||||
...theme.shadows.small,
|
||||
},
|
||||
icon: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.typography.fontSize.displaySmall,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
textAlign: 'center',
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
message: {
|
||||
fontSize: theme.typography.fontSize.bodyLarge,
|
||||
color: theme.colors.textSecondary,
|
||||
textAlign: 'center',
|
||||
lineHeight: theme.typography.lineHeight.relaxed * theme.typography.fontSize.bodyLarge,
|
||||
marginBottom: theme.spacing.xl,
|
||||
maxWidth: width * 0.8,
|
||||
},
|
||||
buttonsContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing.md,
|
||||
marginBottom: theme.spacing.xl,
|
||||
},
|
||||
actionButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.colors.primary,
|
||||
paddingHorizontal: theme.spacing.lg,
|
||||
paddingVertical: theme.spacing.md,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
gap: theme.spacing.sm,
|
||||
...theme.shadows.medium,
|
||||
},
|
||||
actionButtonText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.background,
|
||||
},
|
||||
secondaryButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.primary,
|
||||
paddingHorizontal: theme.spacing.lg,
|
||||
paddingVertical: theme.spacing.md,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
secondaryButtonText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
buttonIcon: {
|
||||
// No additional styles needed
|
||||
},
|
||||
suggestionsContainer: {
|
||||
alignItems: 'center',
|
||||
maxWidth: width * 0.8,
|
||||
},
|
||||
suggestionsTitle: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.textSecondary,
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
suggestionsList: {
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
suggestionItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.sm,
|
||||
paddingVertical: theme.spacing.xs,
|
||||
},
|
||||
suggestionText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textMuted,
|
||||
},
|
||||
});
|
||||
|
||||
export default EmptyState;
|
||||
|
||||
/*
|
||||
* End of File: EmptyState.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
368
app/modules/AIPrediction/components/FilterTabs.tsx
Normal file
368
app/modules/AIPrediction/components/FilterTabs.tsx
Normal file
@ -0,0 +1,368 @@
|
||||
/*
|
||||
* File: FilterTabs.tsx
|
||||
* Description: Filter tabs component for AI predictions
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme';
|
||||
import type { AIPredictionState } from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface FilterTabsProps {
|
||||
selectedUrgencyFilter: AIPredictionState['selectedUrgencyFilter'];
|
||||
selectedSeverityFilter: AIPredictionState['selectedSeverityFilter'];
|
||||
selectedCategoryFilter: AIPredictionState['selectedCategoryFilter'];
|
||||
onUrgencyFilterChange: (filter: AIPredictionState['selectedUrgencyFilter']) => void;
|
||||
onSeverityFilterChange: (filter: AIPredictionState['selectedSeverityFilter']) => void;
|
||||
onCategoryFilterChange: (filter: AIPredictionState['selectedCategoryFilter']) => void;
|
||||
onClearFilters: () => void;
|
||||
filterCounts?: {
|
||||
urgency: Record<string, number>;
|
||||
severity: Record<string, number>;
|
||||
category: Record<string, number>;
|
||||
};
|
||||
activeFiltersCount?: number;
|
||||
}
|
||||
|
||||
interface FilterOption {
|
||||
label: string;
|
||||
value: string;
|
||||
count?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
const URGENCY_FILTERS: FilterOption[] = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Emergency', value: 'emergency', color: '#F44336' },
|
||||
{ label: 'Urgent', value: 'urgent', color: '#FF5722' },
|
||||
{ label: 'Moderate', value: 'moderate', color: '#FF9800' },
|
||||
{ label: 'Low', value: 'low', color: '#FFC107' },
|
||||
{ label: 'Routine', value: 'routine', color: '#4CAF50' },
|
||||
];
|
||||
|
||||
const SEVERITY_FILTERS: FilterOption[] = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'High', value: 'high', color: '#F44336' },
|
||||
{ label: 'Medium', value: 'medium', color: '#FF9800' },
|
||||
{ label: 'Low', value: 'low', color: '#FFC107' },
|
||||
{ label: 'None', value: 'none', color: '#4CAF50' },
|
||||
];
|
||||
|
||||
const CATEGORY_FILTERS: FilterOption[] = [
|
||||
{ label: 'All', value: 'all' },
|
||||
{ label: 'Critical', value: 'critical', color: '#F44336' },
|
||||
{ label: 'Abnormal', value: 'abnormal', color: '#FF9800' },
|
||||
{ label: 'Warning', value: 'warning', color: '#FFC107' },
|
||||
{ label: 'Normal', value: 'normal', color: '#4CAF50' },
|
||||
{ label: 'Unknown', value: 'unknown', color: '#9E9E9E' },
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// FILTER TABS COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* FilterTabs Component
|
||||
*
|
||||
* Purpose: Provide filtering functionality for AI predictions
|
||||
*
|
||||
* Features:
|
||||
* - Multiple filter categories (urgency, severity, category)
|
||||
* - Visual filter counts
|
||||
* - Active filter indicators
|
||||
* - Clear all filters functionality
|
||||
* - Color-coded filter options
|
||||
* - Horizontal scroll support
|
||||
* - Responsive design
|
||||
* - Accessibility support
|
||||
*/
|
||||
const FilterTabs: React.FC<FilterTabsProps> = ({
|
||||
selectedUrgencyFilter,
|
||||
selectedSeverityFilter,
|
||||
selectedCategoryFilter,
|
||||
onUrgencyFilterChange,
|
||||
onSeverityFilterChange,
|
||||
onCategoryFilterChange,
|
||||
onClearFilters,
|
||||
filterCounts,
|
||||
activeFiltersCount = 0,
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get Filter Count
|
||||
*
|
||||
* Purpose: Get count for specific filter value
|
||||
*/
|
||||
const getFilterCount = (category: 'urgency' | 'severity' | 'category', value: string): number => {
|
||||
return filterCounts?.[category]?.[value] || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Render Filter Tab
|
||||
*
|
||||
* Purpose: Render individual filter tab
|
||||
*/
|
||||
const renderFilterTab = (
|
||||
option: FilterOption,
|
||||
isSelected: boolean,
|
||||
onPress: () => void,
|
||||
category: 'urgency' | 'severity' | 'category'
|
||||
) => {
|
||||
const count = getFilterCount(category, option.value);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.value}
|
||||
style={[
|
||||
styles.filterTab,
|
||||
isSelected && styles.selectedFilterTab,
|
||||
isSelected && option.color && { borderColor: option.color },
|
||||
]}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: isSelected }}
|
||||
accessibilityLabel={`Filter by ${option.label}${count > 0 ? `, ${count} items` : ''}`}
|
||||
>
|
||||
{option.color && isSelected && (
|
||||
<View style={[styles.colorIndicator, { backgroundColor: option.color }]} />
|
||||
)}
|
||||
|
||||
<Text style={[
|
||||
styles.filterTabText,
|
||||
isSelected && styles.selectedFilterTabText,
|
||||
isSelected && option.color && { color: option.color },
|
||||
]}>
|
||||
{option.label}
|
||||
</Text>
|
||||
|
||||
{count > 0 && (
|
||||
<View style={[
|
||||
styles.countBadge,
|
||||
isSelected && styles.selectedCountBadge,
|
||||
isSelected && option.color && { backgroundColor: option.color },
|
||||
]}>
|
||||
<Text style={[
|
||||
styles.countText,
|
||||
isSelected && styles.selectedCountText,
|
||||
]}>
|
||||
{count}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Header with Clear Filters */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>Filters</Text>
|
||||
|
||||
{activeFiltersCount > 0 && (
|
||||
<TouchableOpacity
|
||||
style={styles.clearButton}
|
||||
onPress={onClearFilters}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Clear all filters"
|
||||
>
|
||||
<Icon name="x" size={16} color={theme.colors.primary} />
|
||||
<Text style={styles.clearButtonText}>Clear All</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Urgency Filters */}
|
||||
<View style={styles.filterSection}>
|
||||
<Text style={styles.sectionTitle}>Clinical Urgency</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.filterRow}
|
||||
>
|
||||
{URGENCY_FILTERS.map((option) =>
|
||||
renderFilterTab(
|
||||
{ ...option, count: getFilterCount('urgency', option.value) },
|
||||
selectedUrgencyFilter === option.value,
|
||||
() => onUrgencyFilterChange(option.value as AIPredictionState['selectedUrgencyFilter']),
|
||||
'urgency'
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Severity Filters */}
|
||||
<View style={styles.filterSection}>
|
||||
<Text style={styles.sectionTitle}>Primary Severity</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.filterRow}
|
||||
>
|
||||
{SEVERITY_FILTERS.map((option) =>
|
||||
renderFilterTab(
|
||||
{ ...option, count: getFilterCount('severity', option.value) },
|
||||
selectedSeverityFilter === option.value,
|
||||
() => onSeverityFilterChange(option.value as AIPredictionState['selectedSeverityFilter']),
|
||||
'severity'
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Category Filters */}
|
||||
<View style={styles.filterSection}>
|
||||
<Text style={styles.sectionTitle}>Finding Category</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={styles.filterRow}
|
||||
>
|
||||
{CATEGORY_FILTERS.map((option) =>
|
||||
renderFilterTab(
|
||||
{ ...option, count: getFilterCount('category', option.value) },
|
||||
selectedCategoryFilter === option.value,
|
||||
() => onCategoryFilterChange(option.value as AIPredictionState['selectedCategoryFilter']),
|
||||
'category'
|
||||
)
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: theme.colors.background,
|
||||
paddingVertical: theme.spacing.md,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: theme.typography.fontSize.bodyLarge,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
clearButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.xs,
|
||||
paddingHorizontal: theme.spacing.sm,
|
||||
paddingVertical: theme.spacing.xs,
|
||||
borderRadius: theme.borderRadius.small,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.primary,
|
||||
},
|
||||
clearButtonText: {
|
||||
fontSize: theme.typography.fontSize.bodySmall,
|
||||
color: theme.colors.primary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
filterSection: {
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
color: theme.colors.textSecondary,
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
filterRow: {
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
filterTab: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.background,
|
||||
gap: theme.spacing.xs,
|
||||
},
|
||||
selectedFilterTab: {
|
||||
borderColor: theme.colors.primary,
|
||||
backgroundColor: theme.colors.backgroundAccent,
|
||||
},
|
||||
colorIndicator: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
filterTabText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textSecondary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
selectedFilterTabText: {
|
||||
color: theme.colors.primary,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
},
|
||||
countBadge: {
|
||||
backgroundColor: theme.colors.textMuted,
|
||||
borderRadius: theme.borderRadius.small,
|
||||
paddingHorizontal: theme.spacing.xs,
|
||||
paddingVertical: 2,
|
||||
minWidth: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
selectedCountBadge: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
},
|
||||
countText: {
|
||||
fontSize: theme.typography.fontSize.caption,
|
||||
color: theme.colors.background,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
},
|
||||
selectedCountText: {
|
||||
color: theme.colors.background,
|
||||
},
|
||||
});
|
||||
|
||||
export default FilterTabs;
|
||||
|
||||
/*
|
||||
* End of File: FilterTabs.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
139
app/modules/AIPrediction/components/LoadingState.tsx
Normal file
139
app/modules/AIPrediction/components/LoadingState.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* File: LoadingState.tsx
|
||||
* Description: Loading state component for AI predictions
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { theme } from '../../../theme';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface LoadingStateProps {
|
||||
message?: string;
|
||||
showSpinner?: boolean;
|
||||
size?: 'small' | 'large';
|
||||
style?: any;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
// ============================================================================
|
||||
// LOADING STATE COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* LoadingState Component
|
||||
*
|
||||
* Purpose: Display loading state for AI predictions
|
||||
*
|
||||
* Features:
|
||||
* - Customizable loading message
|
||||
* - Optional spinner display
|
||||
* - Different spinner sizes
|
||||
* - Custom styling support
|
||||
* - Centered layout
|
||||
* - Accessibility support
|
||||
*/
|
||||
const LoadingState: React.FC<LoadingStateProps> = ({
|
||||
message = 'Loading AI predictions...',
|
||||
showSpinner = true,
|
||||
size = 'large',
|
||||
style,
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]} accessibilityRole="progressbar">
|
||||
{/* Loading Spinner */}
|
||||
{showSpinner && (
|
||||
<ActivityIndicator
|
||||
size={size}
|
||||
color={theme.colors.primary}
|
||||
style={styles.spinner}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Loading Message */}
|
||||
<Text style={styles.message} accessibilityLabel={message}>
|
||||
{message}
|
||||
</Text>
|
||||
|
||||
{/* Loading Animation Dots */}
|
||||
<View style={styles.dotsContainer}>
|
||||
<View style={[styles.dot, styles.dot1]} />
|
||||
<View style={[styles.dot, styles.dot2]} />
|
||||
<View style={[styles.dot, styles.dot3]} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.xl,
|
||||
paddingVertical: theme.spacing.xxl,
|
||||
minHeight: height * 0.3,
|
||||
},
|
||||
spinner: {
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
message: {
|
||||
fontSize: theme.typography.fontSize.bodyLarge,
|
||||
color: theme.colors.textSecondary,
|
||||
textAlign: 'center',
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
marginBottom: theme.spacing.xl,
|
||||
},
|
||||
dotsContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
dot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: theme.colors.primary,
|
||||
},
|
||||
dot1: {
|
||||
opacity: 0.3,
|
||||
},
|
||||
dot2: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
dot3: {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default LoadingState;
|
||||
|
||||
/*
|
||||
* End of File: LoadingState.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
226
app/modules/AIPrediction/components/SearchBar.tsx
Normal file
226
app/modules/AIPrediction/components/SearchBar.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
/*
|
||||
* File: SearchBar.tsx
|
||||
* Description: Search bar component for filtering AI predictions
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
TextInput,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface SearchBarProps {
|
||||
value: string;
|
||||
onChangeText: (text: string) => void;
|
||||
onClear?: () => void;
|
||||
placeholder?: string;
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
// ============================================================================
|
||||
// SEARCH BAR COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* SearchBar Component
|
||||
*
|
||||
* Purpose: Provide search functionality for AI predictions
|
||||
*
|
||||
* Features:
|
||||
* - Real-time search input
|
||||
* - Clear button functionality
|
||||
* - Customizable placeholder text
|
||||
* - Auto-focus support
|
||||
* - Disabled state support
|
||||
* - Modern design with icons
|
||||
* - Responsive width
|
||||
* - Accessibility support
|
||||
*/
|
||||
const SearchBar: React.FC<SearchBarProps> = ({
|
||||
value,
|
||||
onChangeText,
|
||||
onClear,
|
||||
placeholder = 'Search predictions...',
|
||||
autoFocus = false,
|
||||
disabled = false,
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// STATE
|
||||
// ============================================================================
|
||||
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle Focus
|
||||
*
|
||||
* Purpose: Handle input focus state
|
||||
*/
|
||||
const handleFocus = useCallback(() => {
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle Blur
|
||||
*
|
||||
* Purpose: Handle input blur state
|
||||
*/
|
||||
const handleBlur = useCallback(() => {
|
||||
setIsFocused(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle Clear
|
||||
*
|
||||
* Purpose: Clear search input
|
||||
*/
|
||||
const handleClear = useCallback(() => {
|
||||
onChangeText('');
|
||||
if (onClear) {
|
||||
onClear();
|
||||
}
|
||||
}, [onChangeText, onClear]);
|
||||
|
||||
/**
|
||||
* Handle Text Change
|
||||
*
|
||||
* Purpose: Handle search text input
|
||||
*/
|
||||
const handleTextChange = useCallback((text: string) => {
|
||||
onChangeText(text);
|
||||
}, [onChangeText]);
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<View style={[
|
||||
styles.container,
|
||||
isFocused && styles.focusedContainer,
|
||||
disabled && styles.disabledContainer,
|
||||
]}>
|
||||
{/* Search Icon */}
|
||||
<Icon
|
||||
name="search"
|
||||
size={20}
|
||||
color={isFocused ? theme.colors.primary : theme.colors.textMuted}
|
||||
style={styles.searchIcon}
|
||||
/>
|
||||
|
||||
{/* Text Input */}
|
||||
<TextInput
|
||||
style={[
|
||||
styles.input,
|
||||
disabled && styles.disabledInput,
|
||||
]}
|
||||
value={value}
|
||||
onChangeText={handleTextChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
autoFocus={autoFocus}
|
||||
editable={!disabled}
|
||||
selectTextOnFocus={!disabled}
|
||||
autoCorrect={false}
|
||||
autoCapitalize="none"
|
||||
returnKeyType="search"
|
||||
clearButtonMode="never" // We handle clear button manually
|
||||
accessibilityLabel="Search AI predictions"
|
||||
accessibilityHint="Enter patient ID, finding type, or location to search"
|
||||
/>
|
||||
|
||||
{/* Clear Button */}
|
||||
{value.length > 0 && !disabled && (
|
||||
<TouchableOpacity
|
||||
style={styles.clearButton}
|
||||
onPress={handleClear}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Clear search"
|
||||
accessibilityHint="Clear the search input"
|
||||
>
|
||||
<Icon
|
||||
name="x"
|
||||
size={18}
|
||||
color={theme.colors.textMuted}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.colors.background,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
marginHorizontal: theme.spacing.md,
|
||||
marginVertical: theme.spacing.sm,
|
||||
...theme.shadows.small,
|
||||
},
|
||||
focusedContainer: {
|
||||
borderColor: theme.colors.primary,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
disabledContainer: {
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
opacity: 0.6,
|
||||
},
|
||||
searchIcon: {
|
||||
marginRight: theme.spacing.sm,
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textPrimary,
|
||||
paddingVertical: 0, // Remove default padding to maintain consistent height
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
},
|
||||
disabledInput: {
|
||||
color: theme.colors.textMuted,
|
||||
},
|
||||
clearButton: {
|
||||
marginLeft: theme.spacing.sm,
|
||||
padding: theme.spacing.xs,
|
||||
},
|
||||
});
|
||||
|
||||
export default SearchBar;
|
||||
|
||||
/*
|
||||
* End of File: SearchBar.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
454
app/modules/AIPrediction/components/StatsOverview.tsx
Normal file
454
app/modules/AIPrediction/components/StatsOverview.tsx
Normal file
@ -0,0 +1,454 @@
|
||||
/*
|
||||
* File: StatsOverview.tsx
|
||||
* Description: Statistics overview component for AI predictions dashboard
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme';
|
||||
import type { AIPredictionStats } from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface StatsOverviewProps {
|
||||
stats: AIPredictionStats;
|
||||
onStatsPress?: (statType: string) => void;
|
||||
isLoading?: boolean;
|
||||
style?: any;
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
value: string | number;
|
||||
subtitle?: string;
|
||||
iconName: string;
|
||||
color: string;
|
||||
onPress?: () => void;
|
||||
trend?: number;
|
||||
isPercentage?: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const CARD_WIDTH = (width - 48) / 2; // Two cards per row with margins
|
||||
|
||||
// ============================================================================
|
||||
// STAT CARD COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* StatCard Component
|
||||
*
|
||||
* Purpose: Individual statistics card
|
||||
*/
|
||||
const StatCard: React.FC<StatCardProps> = ({
|
||||
title,
|
||||
value,
|
||||
subtitle,
|
||||
iconName,
|
||||
color,
|
||||
onPress,
|
||||
trend,
|
||||
isPercentage = false,
|
||||
}) => {
|
||||
const displayValue = typeof value === 'number'
|
||||
? isPercentage
|
||||
? `${Math.round(value * 100)}%`
|
||||
: value.toLocaleString()
|
||||
: value;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.statCard, { borderLeftColor: color }]}
|
||||
onPress={onPress}
|
||||
disabled={!onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${title}: ${displayValue}${subtitle ? `, ${subtitle}` : ''}`}
|
||||
>
|
||||
{/* Card Header */}
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', gap: theme.spacing.sm}}>
|
||||
<View style={[styles.iconContainer, { backgroundColor: color + '20' }]}>
|
||||
<Icon name={iconName} size={20} color={color} />
|
||||
</View>
|
||||
<Text style={styles.statValue}>{displayValue}</Text>
|
||||
</View>
|
||||
|
||||
|
||||
{trend !== undefined && (
|
||||
<View style={styles.trendContainer}>
|
||||
<Icon
|
||||
name={trend >= 0 ? 'trending-up' : 'trending-down'}
|
||||
size={14}
|
||||
color={trend >= 0 ? theme.colors.success : theme.colors.error}
|
||||
/>
|
||||
<Text style={[
|
||||
styles.trendText,
|
||||
{ color: trend >= 0 ? theme.colors.success : theme.colors.error }
|
||||
]}>
|
||||
{Math.abs(trend).toFixed(1)}%
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Card Content */}
|
||||
<View style={styles.cardContent}>
|
||||
|
||||
<Text style={styles.statTitle} numberOfLines={2}>{title}</Text>
|
||||
{subtitle && (
|
||||
<Text style={styles.statSubtitle} numberOfLines={1}>{subtitle}</Text>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STATS OVERVIEW COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* StatsOverview Component
|
||||
*
|
||||
* Purpose: Display comprehensive AI predictions statistics
|
||||
*
|
||||
* Features:
|
||||
* - Total cases overview
|
||||
* - Critical and urgent case counts
|
||||
* - Review progress tracking
|
||||
* - Average confidence metrics
|
||||
* - Trend indicators
|
||||
* - Interactive stat cards
|
||||
* - Responsive grid layout
|
||||
* - Modern card design
|
||||
* - Accessibility support
|
||||
*/
|
||||
const StatsOverview: React.FC<StatsOverviewProps> = ({
|
||||
stats,
|
||||
onStatsPress,
|
||||
isLoading = false,
|
||||
style,
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle Stat Press
|
||||
*
|
||||
* Purpose: Handle statistics card press
|
||||
*/
|
||||
const handleStatPress = (statType: string) => {
|
||||
if (onStatsPress) {
|
||||
onStatsPress(statType);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.sectionTitle}>AI Predictions Overview</Text>
|
||||
</View>
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>Loading statistics...</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
{/* Section Header */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.sectionTitle}>AI Predictions Overview</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.viewAllButton}
|
||||
onPress={() => handleStatPress('all')}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="View all statistics"
|
||||
>
|
||||
<Text style={styles.viewAllText}>View All</Text>
|
||||
<Icon name="arrow-right" size={16} color={theme.colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Statistics Grid */}
|
||||
<View style={styles.statsGrid}>
|
||||
{/* Total Cases */}
|
||||
<StatCard
|
||||
title="Total Cases"
|
||||
value={stats.totalCases}
|
||||
subtitle="All predictions"
|
||||
iconName="database"
|
||||
color={theme.colors.primary}
|
||||
onPress={() => handleStatPress('total')}
|
||||
/>
|
||||
|
||||
{/* Critical Cases */}
|
||||
<StatCard
|
||||
title="Critical Cases"
|
||||
value={stats.criticalCases}
|
||||
subtitle="Require attention"
|
||||
iconName="alert-triangle"
|
||||
color={theme.colors.error}
|
||||
onPress={() => handleStatPress('critical')}
|
||||
/>
|
||||
|
||||
{/* Urgent Cases */}
|
||||
<StatCard
|
||||
title="Urgent Cases"
|
||||
value={stats.urgentCases}
|
||||
subtitle="High priority"
|
||||
iconName="clock"
|
||||
color={theme.colors.warning}
|
||||
onPress={() => handleStatPress('urgent')}
|
||||
/>
|
||||
|
||||
{/* Reviewed Cases */}
|
||||
<StatCard
|
||||
title="Reviewed Cases"
|
||||
value={stats.reviewedCases}
|
||||
subtitle="Completed reviews"
|
||||
iconName="check-circle"
|
||||
color={theme.colors.success}
|
||||
onPress={() => handleStatPress('reviewed')}
|
||||
/>
|
||||
|
||||
{/* Pending Cases */}
|
||||
<StatCard
|
||||
title="Pending Reviews"
|
||||
value={stats.pendingCases}
|
||||
subtitle="Awaiting review"
|
||||
iconName="eye"
|
||||
color={theme.colors.info}
|
||||
onPress={() => handleStatPress('pending')}
|
||||
/>
|
||||
|
||||
{/* Average Confidence */}
|
||||
<StatCard
|
||||
title="Avg Confidence"
|
||||
value={stats.averageConfidence}
|
||||
subtitle="AI accuracy"
|
||||
iconName="trending-up"
|
||||
color={theme.colors.primary}
|
||||
onPress={() => handleStatPress('confidence')}
|
||||
isPercentage={true}
|
||||
/>
|
||||
|
||||
{/* Today's Cases */}
|
||||
<StatCard
|
||||
title="Today's Cases"
|
||||
value={stats.todaysCases}
|
||||
subtitle="New predictions"
|
||||
iconName="calendar"
|
||||
color={theme.colors.info}
|
||||
onPress={() => handleStatPress('today')}
|
||||
/>
|
||||
|
||||
{/* Weekly Trend */}
|
||||
<StatCard
|
||||
title="Weekly Trend"
|
||||
value={`${stats.weeklyTrend >= 0 ? '+' : ''}${stats.weeklyTrend.toFixed(1)}%`}
|
||||
subtitle="vs last week"
|
||||
iconName={stats.weeklyTrend >= 0 ? 'trending-up' : 'trending-down'}
|
||||
color={stats.weeklyTrend >= 0 ? theme.colors.success : theme.colors.error}
|
||||
onPress={() => handleStatPress('trend')}
|
||||
trend={stats.weeklyTrend}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Summary Section */}
|
||||
<View style={styles.summarySection}>
|
||||
<View style={styles.summaryCard}>
|
||||
<View style={styles.summaryHeader}>
|
||||
<Icon name="activity" size={20} color={theme.colors.primary} />
|
||||
<Text style={styles.summaryTitle}>Quick Insights</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.summaryContent}>
|
||||
<View style={styles.summaryItem}>
|
||||
<Text style={styles.summaryLabel}>Review Progress:</Text>
|
||||
<Text style={styles.summaryValue}>
|
||||
{Math.round((stats.reviewedCases / stats.totalCases) * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.summaryItem}>
|
||||
<Text style={styles.summaryLabel}>Critical Rate:</Text>
|
||||
<Text style={styles.summaryValue}>
|
||||
{Math.round((stats.criticalCases / stats.totalCases) * 100)}%
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.summaryItem}>
|
||||
<Text style={styles.summaryLabel}>Daily Average:</Text>
|
||||
<Text style={styles.summaryValue}>
|
||||
{Math.round(stats.totalCases / 7)} cases
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: theme.colors.background,
|
||||
paddingVertical: theme.spacing.lg,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: theme.typography.fontSize.displaySmall,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
viewAllButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.xs,
|
||||
},
|
||||
viewAllText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.primary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
loadingContainer: {
|
||||
paddingVertical: theme.spacing.xxl,
|
||||
alignItems: 'center',
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textMuted,
|
||||
},
|
||||
statsGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
gap: theme.spacing.md,
|
||||
},
|
||||
statCard: {
|
||||
width: CARD_WIDTH,
|
||||
backgroundColor: theme.colors.background,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
borderLeftWidth: 4,
|
||||
padding: theme.spacing.md,
|
||||
...theme.shadows.medium,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
trendContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.xs,
|
||||
},
|
||||
trendText: {
|
||||
fontSize: theme.typography.fontSize.caption,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
cardContent: {
|
||||
gap: theme.spacing.xs,
|
||||
},
|
||||
statValue: {
|
||||
fontSize: theme.typography.fontSize.displayMedium,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
statTitle: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textSecondary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
statSubtitle: {
|
||||
fontSize: theme.typography.fontSize.bodySmall,
|
||||
color: theme.colors.textMuted,
|
||||
},
|
||||
summarySection: {
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
marginTop: theme.spacing.lg,
|
||||
},
|
||||
summaryCard: {
|
||||
backgroundColor: theme.colors.background,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
padding: theme.spacing.lg,
|
||||
...theme.shadows.small,
|
||||
},
|
||||
summaryHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.sm,
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
summaryTitle: {
|
||||
fontSize: theme.typography.fontSize.bodyLarge,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
summaryContent: {
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
summaryItem: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
summaryLabel: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textSecondary,
|
||||
},
|
||||
summaryValue: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
});
|
||||
|
||||
export default StatsOverview;
|
||||
|
||||
/*
|
||||
* End of File: StatsOverview.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
19
app/modules/AIPrediction/components/index.ts
Normal file
19
app/modules/AIPrediction/components/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Components exports for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export { default as AIPredictionCard } from './AIPredictionCard';
|
||||
export { default as SearchBar } from './SearchBar';
|
||||
export { default as FilterTabs } from './FilterTabs';
|
||||
export { default as LoadingState } from './LoadingState';
|
||||
export { default as EmptyState } from './EmptyState';
|
||||
export { default as StatsOverview } from './StatsOverview';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
14
app/modules/AIPrediction/hooks/index.ts
Normal file
14
app/modules/AIPrediction/hooks/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Hooks exports for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export * from './useAIPredictions';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
383
app/modules/AIPrediction/hooks/useAIPredictions.ts
Normal file
383
app/modules/AIPrediction/hooks/useAIPredictions.ts
Normal file
@ -0,0 +1,383 @@
|
||||
/*
|
||||
* File: useAIPredictions.ts
|
||||
* Description: Custom hook for AI Predictions functionality
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
|
||||
// Import Redux actions and selectors
|
||||
import {
|
||||
fetchAIPredictions,
|
||||
setSearchQuery,
|
||||
setUrgencyFilter,
|
||||
setSeverityFilter,
|
||||
setCategoryFilter,
|
||||
clearAllFilters,
|
||||
updateCaseReview,
|
||||
} from '../redux';
|
||||
|
||||
import {
|
||||
selectPaginatedCases,
|
||||
selectIsLoading,
|
||||
selectError,
|
||||
selectSearchQuery,
|
||||
selectUrgencyFilter,
|
||||
selectSeverityFilter,
|
||||
selectCategoryFilter,
|
||||
selectCasesStatistics,
|
||||
selectActiveFiltersCount,
|
||||
selectCurrentPage,
|
||||
selectTotalPages,
|
||||
} from '../redux';
|
||||
|
||||
// Import auth selector
|
||||
import { selectUser } from '../../Auth/redux/authSelectors';
|
||||
|
||||
// Import types
|
||||
import type { AIPredictionState } from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface UseAIPredictionsOptions {
|
||||
autoLoad?: boolean;
|
||||
refreshInterval?: number;
|
||||
}
|
||||
|
||||
interface UseAIPredictionsReturn {
|
||||
// Data
|
||||
cases: ReturnType<typeof selectPaginatedCases>;
|
||||
statistics: ReturnType<typeof selectCasesStatistics>;
|
||||
|
||||
// Loading states
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Filters
|
||||
searchQuery: string;
|
||||
urgencyFilter: AIPredictionState['selectedUrgencyFilter'];
|
||||
severityFilter: AIPredictionState['selectedSeverityFilter'];
|
||||
categoryFilter: AIPredictionState['selectedCategoryFilter'];
|
||||
activeFiltersCount: number;
|
||||
|
||||
// Pagination
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
|
||||
// Actions
|
||||
loadPredictions: () => Promise<void>;
|
||||
refreshPredictions: () => Promise<void>;
|
||||
setSearch: (query: string) => void;
|
||||
setUrgency: (filter: AIPredictionState['selectedUrgencyFilter']) => void;
|
||||
setSeverity: (filter: AIPredictionState['selectedSeverityFilter']) => void;
|
||||
setCategory: (filter: AIPredictionState['selectedCategoryFilter']) => void;
|
||||
clearFilters: () => void;
|
||||
reviewCase: (caseId: string, reviewData?: Partial<{
|
||||
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
|
||||
reviewed_by: string;
|
||||
review_notes: string;
|
||||
priority: 'critical' | 'high' | 'medium' | 'low';
|
||||
}>) => Promise<void>;
|
||||
|
||||
// Computed properties
|
||||
hasFilters: boolean;
|
||||
isEmpty: boolean;
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USE AI PREDICTIONS HOOK
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* useAIPredictions Hook
|
||||
*
|
||||
* Purpose: Custom hook for managing AI predictions state and actions
|
||||
*
|
||||
* Features:
|
||||
* - Automatic data loading on mount
|
||||
* - Search and filtering functionality
|
||||
* - Case review management
|
||||
* - Error handling
|
||||
* - Loading states
|
||||
* - Computed properties for UI state
|
||||
* - Auto-refresh capability
|
||||
* - Type-safe actions and selectors
|
||||
*/
|
||||
export const useAIPredictions = (options: UseAIPredictionsOptions = {}): UseAIPredictionsReturn => {
|
||||
const {
|
||||
autoLoad = true,
|
||||
refreshInterval,
|
||||
} = options;
|
||||
|
||||
// ============================================================================
|
||||
// REDUX STATE
|
||||
// ============================================================================
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Auth state
|
||||
const user = useAppSelector(selectUser);
|
||||
|
||||
// AI Predictions state
|
||||
const cases = useAppSelector(selectPaginatedCases);
|
||||
const statistics = useAppSelector(selectCasesStatistics);
|
||||
const isLoading = useAppSelector(selectIsLoading);
|
||||
const error = useAppSelector(selectError);
|
||||
const searchQuery = useAppSelector(selectSearchQuery);
|
||||
const urgencyFilter = useAppSelector(selectUrgencyFilter);
|
||||
const severityFilter = useAppSelector(selectSeverityFilter);
|
||||
const categoryFilter = useAppSelector(selectCategoryFilter);
|
||||
const activeFiltersCount = useAppSelector(selectActiveFiltersCount);
|
||||
const currentPage = useAppSelector(selectCurrentPage);
|
||||
const totalPages = useAppSelector(selectTotalPages);
|
||||
|
||||
// ============================================================================
|
||||
// MEMOIZED VALUES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Has Filters
|
||||
*
|
||||
* Purpose: Check if any filters are active
|
||||
*/
|
||||
const hasFilters = useMemo(() => activeFiltersCount > 0, [activeFiltersCount]);
|
||||
|
||||
/**
|
||||
* Is Empty
|
||||
*
|
||||
* Purpose: Check if the cases list is empty
|
||||
*/
|
||||
const isEmpty = useMemo(() => cases.length === 0, [cases.length]);
|
||||
|
||||
/**
|
||||
* Has Error
|
||||
*
|
||||
* Purpose: Check if there's an error
|
||||
*/
|
||||
const hasError = useMemo(() => error !== null, [error]);
|
||||
|
||||
// ============================================================================
|
||||
// ACTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load Predictions
|
||||
*
|
||||
* Purpose: Load AI predictions from API
|
||||
*/
|
||||
const loadPredictions = useCallback(async () => {
|
||||
|
||||
if (!user?.access_token) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage,
|
||||
limit: 20,
|
||||
...(urgencyFilter !== 'all' && { urgency: urgencyFilter }),
|
||||
...(severityFilter !== 'all' && { severity: severityFilter }),
|
||||
...(categoryFilter !== 'all' && { category: categoryFilter }),
|
||||
...(searchQuery.trim() && { search: searchQuery.trim() }),
|
||||
};
|
||||
|
||||
await dispatch(fetchAIPredictions({
|
||||
token: user.access_token,
|
||||
params,
|
||||
})).unwrap();
|
||||
} catch (error) {
|
||||
console.error('Failed to load AI predictions:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [
|
||||
dispatch,
|
||||
user?.access_token,
|
||||
currentPage,
|
||||
urgencyFilter,
|
||||
severityFilter,
|
||||
categoryFilter,
|
||||
searchQuery,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Refresh Predictions
|
||||
*
|
||||
* Purpose: Refresh AI predictions data
|
||||
*/
|
||||
const refreshPredictions = useCallback(async () => {
|
||||
await loadPredictions();
|
||||
}, [loadPredictions]);
|
||||
|
||||
/**
|
||||
* Set Search
|
||||
*
|
||||
* Purpose: Set search query
|
||||
*/
|
||||
const setSearch = useCallback((query: string) => {
|
||||
dispatch(setSearchQuery(query));
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Set Urgency Filter
|
||||
*
|
||||
* Purpose: Set urgency filter
|
||||
*/
|
||||
const setUrgency = useCallback((filter: AIPredictionState['selectedUrgencyFilter']) => {
|
||||
dispatch(setUrgencyFilter(filter));
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Set Severity Filter
|
||||
*
|
||||
* Purpose: Set severity filter
|
||||
*/
|
||||
const setSeverity = useCallback((filter: AIPredictionState['selectedSeverityFilter']) => {
|
||||
dispatch(setSeverityFilter(filter));
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Set Category Filter
|
||||
*
|
||||
* Purpose: Set category filter
|
||||
*/
|
||||
const setCategory = useCallback((filter: AIPredictionState['selectedCategoryFilter']) => {
|
||||
dispatch(setCategoryFilter(filter));
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Clear Filters
|
||||
*
|
||||
* Purpose: Clear all active filters
|
||||
*/
|
||||
const clearFilters = useCallback(() => {
|
||||
dispatch(clearAllFilters());
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Review Case
|
||||
*
|
||||
* Purpose: Update case review status
|
||||
*/
|
||||
const reviewCase = useCallback(async (
|
||||
caseId: string,
|
||||
reviewData: Partial<{
|
||||
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
|
||||
reviewed_by: string;
|
||||
review_notes: string;
|
||||
priority: 'critical' | 'high' | 'medium' | 'low';
|
||||
}> = {}
|
||||
) => {
|
||||
if (!user?.access_token) {
|
||||
throw new Error('User not authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
const defaultReviewData = {
|
||||
review_status: 'reviewed' as const,
|
||||
reviewed_by: user.display_name || user.email || 'Current User',
|
||||
...reviewData,
|
||||
};
|
||||
|
||||
await dispatch(updateCaseReview({
|
||||
caseId,
|
||||
reviewData: defaultReviewData,
|
||||
token: user.access_token,
|
||||
})).unwrap();
|
||||
} catch (error) {
|
||||
console.error('Failed to review case:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [dispatch, user]);
|
||||
|
||||
// ============================================================================
|
||||
// EFFECTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Auto-load Effect
|
||||
*
|
||||
* Purpose: Automatically load predictions on mount if enabled
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (autoLoad && user?.access_token) {
|
||||
loadPredictions().catch(console.error);
|
||||
}
|
||||
}, [autoLoad, user?.access_token, loadPredictions]);
|
||||
|
||||
/**
|
||||
* Auto-refresh Effect
|
||||
*
|
||||
* Purpose: Set up auto-refresh interval if specified
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!refreshInterval || !user?.access_token) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
loadPredictions().catch(console.error);
|
||||
}, refreshInterval);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [refreshInterval, user?.access_token, loadPredictions]);
|
||||
|
||||
/**
|
||||
* Filter Change Effect
|
||||
*
|
||||
* Purpose: Reload data when filters change
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (user?.access_token) {
|
||||
loadPredictions().catch(console.error);
|
||||
}
|
||||
}, [urgencyFilter, severityFilter, categoryFilter, searchQuery, currentPage]);
|
||||
|
||||
// ============================================================================
|
||||
// RETURN
|
||||
// ============================================================================
|
||||
|
||||
return {
|
||||
// Data
|
||||
cases,
|
||||
statistics,
|
||||
|
||||
// Loading states
|
||||
isLoading,
|
||||
error,
|
||||
|
||||
// Filters
|
||||
searchQuery,
|
||||
urgencyFilter,
|
||||
severityFilter,
|
||||
categoryFilter,
|
||||
activeFiltersCount,
|
||||
|
||||
// Pagination
|
||||
currentPage,
|
||||
totalPages,
|
||||
|
||||
// Actions
|
||||
loadPredictions,
|
||||
refreshPredictions,
|
||||
setSearch,
|
||||
setUrgency,
|
||||
setSeverity,
|
||||
setCategory,
|
||||
clearFilters,
|
||||
reviewCase,
|
||||
|
||||
// Computed properties
|
||||
hasFilters,
|
||||
isEmpty,
|
||||
hasError,
|
||||
};
|
||||
};
|
||||
|
||||
/*
|
||||
* End of File: useAIPredictions.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
54
app/modules/AIPrediction/index.ts
Normal file
54
app/modules/AIPrediction/index.ts
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Main exports for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// COMPONENT EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export * from './components';
|
||||
|
||||
// ============================================================================
|
||||
// SCREEN EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export * from './screens';
|
||||
|
||||
// ============================================================================
|
||||
// NAVIGATION EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export * from './navigation';
|
||||
|
||||
// ============================================================================
|
||||
// REDUX EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export * from './redux';
|
||||
|
||||
// ============================================================================
|
||||
// SERVICE EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export * from './services';
|
||||
|
||||
// ============================================================================
|
||||
// TYPE EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export * from './types';
|
||||
|
||||
// ============================================================================
|
||||
// HOOK EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export * from './hooks';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -0,0 +1,244 @@
|
||||
/*
|
||||
* File: AIPredictionStackNavigator.tsx
|
||||
* Description: Stack navigator for AI Prediction module screens
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme';
|
||||
|
||||
// Import screens
|
||||
import { AIPredictionsScreen } from '../screens';
|
||||
import { ComingSoonScreen } from '../../../shared/components';
|
||||
|
||||
// Import types
|
||||
import type { AIPredictionStackParamList } from './navigationTypes';
|
||||
|
||||
// ============================================================================
|
||||
// STACK NAVIGATOR SETUP
|
||||
// ============================================================================
|
||||
|
||||
const Stack = createStackNavigator<AIPredictionStackParamList>();
|
||||
|
||||
// ============================================================================
|
||||
// HEADER COMPONENTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Header Back Button
|
||||
*
|
||||
* Purpose: Custom back button for navigation header
|
||||
*/
|
||||
const HeaderBackButton: React.FC<{ onPress: () => void }> = ({ onPress }) => (
|
||||
<TouchableOpacity style={styles.headerButton} onPress={onPress}>
|
||||
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
/**
|
||||
* Header Action Button
|
||||
*
|
||||
* Purpose: Custom action button for navigation header
|
||||
*/
|
||||
const HeaderActionButton: React.FC<{
|
||||
iconName: string;
|
||||
onPress: () => void;
|
||||
accessibilityLabel?: string;
|
||||
}> = ({ iconName, onPress, accessibilityLabel }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={onPress}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
<Icon name={iconName} size={24} color={theme.colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// SCREEN OPTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Default Screen Options
|
||||
*
|
||||
* Purpose: Common screen options for all AI prediction screens
|
||||
*/
|
||||
const defaultScreenOptions = {
|
||||
headerStyle: {
|
||||
backgroundColor: theme.colors.background,
|
||||
elevation: 2,
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
fontSize: theme.typography.fontSize.bodyLarge,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
headerTintColor: theme.colors.textPrimary,
|
||||
headerBackTitleVisible: false,
|
||||
gestureEnabled: true,
|
||||
cardStyleInterpolator: ({ current, layouts }: any) => {
|
||||
return {
|
||||
cardStyle: {
|
||||
transform: [
|
||||
{
|
||||
translateX: current.progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [layouts.screen.width, 0],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// AI PREDICTION STACK NAVIGATOR COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AIPredictionStackNavigator Component
|
||||
*
|
||||
* Purpose: Stack navigator for AI prediction module
|
||||
*
|
||||
* Features:
|
||||
* - AI Prediction List screen (main screen)
|
||||
* - AI Prediction Details screen (case details)
|
||||
* - AI Prediction Filters screen (advanced filtering)
|
||||
* - AI Prediction Stats screen (detailed statistics)
|
||||
* - Custom header styling and buttons
|
||||
* - Smooth navigation transitions
|
||||
* - Accessibility support
|
||||
* - Coming soon screens for unimplemented features
|
||||
*/
|
||||
const AIPredictionStackNavigator: React.FC = () => {
|
||||
return (
|
||||
<Stack.Navigator
|
||||
initialRouteName="AIPredictionList"
|
||||
screenOptions={defaultScreenOptions}
|
||||
>
|
||||
{/* AI Prediction List Screen */}
|
||||
<Stack.Screen
|
||||
name="AIPredictionList"
|
||||
component={AIPredictionsScreen}
|
||||
options={({ navigation }) => ({
|
||||
title: 'AI Predictions',
|
||||
headerLeft: () => null, // No back button on main screen
|
||||
headerRight: () => (
|
||||
<HeaderActionButton
|
||||
iconName="more-vertical"
|
||||
onPress={() => {
|
||||
// Open options menu
|
||||
// For now, just navigate to stats
|
||||
// @ts-ignore
|
||||
navigation.navigate('AIPredictionStats');
|
||||
}}
|
||||
accessibilityLabel="More options"
|
||||
/>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* AI Prediction Details Screen */}
|
||||
<Stack.Screen
|
||||
name="AIPredictionDetails"
|
||||
component={ComingSoonScreen}
|
||||
options={({ navigation, route }) => ({
|
||||
title: 'Prediction Details',
|
||||
headerLeft: () => (
|
||||
<HeaderBackButton onPress={() => navigation.goBack()} />
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderActionButton
|
||||
iconName="share-2"
|
||||
onPress={() => {
|
||||
// Share prediction details
|
||||
console.log('Share prediction:', route.params?.caseId);
|
||||
}}
|
||||
accessibilityLabel="Share prediction"
|
||||
/>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* AI Prediction Filters Screen */}
|
||||
<Stack.Screen
|
||||
name="AIPredictionFilters"
|
||||
component={ComingSoonScreen}
|
||||
options={({ navigation }) => ({
|
||||
title: 'Advanced Filters',
|
||||
headerLeft: () => (
|
||||
<HeaderBackButton onPress={() => navigation.goBack()} />
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderActionButton
|
||||
iconName="refresh-cw"
|
||||
onPress={() => {
|
||||
// Reset filters
|
||||
console.log('Reset filters');
|
||||
}}
|
||||
accessibilityLabel="Reset filters"
|
||||
/>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* AI Prediction Stats Screen */}
|
||||
<Stack.Screen
|
||||
name="AIPredictionStats"
|
||||
component={ComingSoonScreen}
|
||||
options={({ navigation, route }) => ({
|
||||
title: 'Statistics',
|
||||
headerLeft: () => (
|
||||
<HeaderBackButton onPress={() => navigation.goBack()} />
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderActionButton
|
||||
iconName="download"
|
||||
onPress={() => {
|
||||
// Export statistics
|
||||
console.log('Export stats:', route.params?.timeRange);
|
||||
}}
|
||||
accessibilityLabel="Export statistics"
|
||||
/>
|
||||
),
|
||||
})}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
headerButton: {
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
marginHorizontal: theme.spacing.xs,
|
||||
},
|
||||
headerButtonText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.primary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
});
|
||||
|
||||
export default AIPredictionStackNavigator;
|
||||
|
||||
/*
|
||||
* End of File: AIPredictionStackNavigator.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
16
app/modules/AIPrediction/navigation/index.ts
Normal file
16
app/modules/AIPrediction/navigation/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Navigation exports for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export { default as AIPredictionStackNavigator } from './AIPredictionStackNavigator';
|
||||
export * from './navigationTypes';
|
||||
export * from './navigationUtils';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
169
app/modules/AIPrediction/navigation/navigationTypes.ts
Normal file
169
app/modules/AIPrediction/navigation/navigationTypes.ts
Normal file
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* File: navigationTypes.ts
|
||||
* Description: Navigation type definitions for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import type { StackNavigationProp } from '@react-navigation/stack';
|
||||
import type { RouteProp } from '@react-navigation/native';
|
||||
|
||||
// ============================================================================
|
||||
// AI PREDICTION STACK PARAM LIST
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AI Prediction Stack Param List
|
||||
*
|
||||
* Purpose: Define navigation parameters for AI prediction screens
|
||||
*
|
||||
* Screens:
|
||||
* - AIPredictionList: Main list of AI predictions
|
||||
* - AIPredictionDetails: Detailed view of a specific prediction
|
||||
* - AIPredictionFilters: Advanced filtering options
|
||||
* - AIPredictionStats: Detailed statistics view
|
||||
*/
|
||||
export type AIPredictionStackParamList = {
|
||||
AIPredictionList: undefined;
|
||||
AIPredictionDetails: { caseId: string };
|
||||
AIPredictionFilters: undefined;
|
||||
AIPredictionStats: { timeRange?: 'today' | 'week' | 'month' };
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// NAVIGATION PROP TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AI Prediction List Navigation Prop
|
||||
*
|
||||
* Purpose: Navigation prop type for AI prediction list screen
|
||||
*/
|
||||
export type AIPredictionListNavigationProp = StackNavigationProp<
|
||||
AIPredictionStackParamList,
|
||||
'AIPredictionList'
|
||||
>;
|
||||
|
||||
/**
|
||||
* AI Prediction Details Navigation Prop
|
||||
*
|
||||
* Purpose: Navigation prop type for AI prediction details screen
|
||||
*/
|
||||
export type AIPredictionDetailsNavigationProp = StackNavigationProp<
|
||||
AIPredictionStackParamList,
|
||||
'AIPredictionDetails'
|
||||
>;
|
||||
|
||||
/**
|
||||
* AI Prediction Filters Navigation Prop
|
||||
*
|
||||
* Purpose: Navigation prop type for AI prediction filters screen
|
||||
*/
|
||||
export type AIPredictionFiltersNavigationProp = StackNavigationProp<
|
||||
AIPredictionStackParamList,
|
||||
'AIPredictionFilters'
|
||||
>;
|
||||
|
||||
/**
|
||||
* AI Prediction Stats Navigation Prop
|
||||
*
|
||||
* Purpose: Navigation prop type for AI prediction statistics screen
|
||||
*/
|
||||
export type AIPredictionStatsNavigationProp = StackNavigationProp<
|
||||
AIPredictionStackParamList,
|
||||
'AIPredictionStats'
|
||||
>;
|
||||
|
||||
// ============================================================================
|
||||
// ROUTE PROP TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AI Prediction List Route Prop
|
||||
*
|
||||
* Purpose: Route prop type for AI prediction list screen
|
||||
*/
|
||||
export type AIPredictionListRouteProp = RouteProp<
|
||||
AIPredictionStackParamList,
|
||||
'AIPredictionList'
|
||||
>;
|
||||
|
||||
/**
|
||||
* AI Prediction Details Route Prop
|
||||
*
|
||||
* Purpose: Route prop type for AI prediction details screen
|
||||
*/
|
||||
export type AIPredictionDetailsRouteProp = RouteProp<
|
||||
AIPredictionStackParamList,
|
||||
'AIPredictionDetails'
|
||||
>;
|
||||
|
||||
/**
|
||||
* AI Prediction Filters Route Prop
|
||||
*
|
||||
* Purpose: Route prop type for AI prediction filters screen
|
||||
*/
|
||||
export type AIPredictionFiltersRouteProp = RouteProp<
|
||||
AIPredictionStackParamList,
|
||||
'AIPredictionFilters'
|
||||
>;
|
||||
|
||||
/**
|
||||
* AI Prediction Stats Route Prop
|
||||
*
|
||||
* Purpose: Route prop type for AI prediction statistics screen
|
||||
*/
|
||||
export type AIPredictionStatsRouteProp = RouteProp<
|
||||
AIPredictionStackParamList,
|
||||
'AIPredictionStats'
|
||||
>;
|
||||
|
||||
// ============================================================================
|
||||
// COMBINED PROP TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AI Prediction List Screen Props
|
||||
*
|
||||
* Purpose: Combined props for AI prediction list screen
|
||||
*/
|
||||
export interface AIPredictionListScreenProps {
|
||||
navigation: AIPredictionListNavigationProp;
|
||||
route: AIPredictionListRouteProp;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Prediction Details Screen Props
|
||||
*
|
||||
* Purpose: Combined props for AI prediction details screen
|
||||
*/
|
||||
export interface AIPredictionDetailsScreenProps {
|
||||
navigation: AIPredictionDetailsNavigationProp;
|
||||
route: AIPredictionDetailsRouteProp;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Prediction Filters Screen Props
|
||||
*
|
||||
* Purpose: Combined props for AI prediction filters screen
|
||||
*/
|
||||
export interface AIPredictionFiltersScreenProps {
|
||||
navigation: AIPredictionFiltersNavigationProp;
|
||||
route: AIPredictionFiltersRouteProp;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Prediction Stats Screen Props
|
||||
*
|
||||
* Purpose: Combined props for AI prediction statistics screen
|
||||
*/
|
||||
export interface AIPredictionStatsScreenProps {
|
||||
navigation: AIPredictionStatsNavigationProp;
|
||||
route: AIPredictionStatsRouteProp;
|
||||
}
|
||||
|
||||
/*
|
||||
* End of File: navigationTypes.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
251
app/modules/AIPrediction/navigation/navigationUtils.ts
Normal file
251
app/modules/AIPrediction/navigation/navigationUtils.ts
Normal file
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* File: navigationUtils.ts
|
||||
* Description: Navigation utility functions for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
import type { AIPredictionStackParamList } from './navigationTypes';
|
||||
|
||||
// ============================================================================
|
||||
// NAVIGATION UTILITY FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Navigate to AI Prediction Details
|
||||
*
|
||||
* Purpose: Navigate to AI prediction case details screen
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @param caseId - AI prediction case ID
|
||||
*/
|
||||
export const navigateToAIPredictionDetails = (
|
||||
navigation: any,
|
||||
caseId: string
|
||||
) => {
|
||||
navigation.navigate('AIPredictionDetails', { caseId });
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to AI Prediction Filters
|
||||
*
|
||||
* Purpose: Navigate to advanced filters screen
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
*/
|
||||
export const navigateToAIPredictionFilters = (navigation: any) => {
|
||||
navigation.navigate('AIPredictionFilters');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate to AI Prediction Statistics
|
||||
*
|
||||
* Purpose: Navigate to detailed statistics screen
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @param timeRange - Optional time range filter
|
||||
*/
|
||||
export const navigateToAIPredictionStats = (
|
||||
navigation: any,
|
||||
timeRange?: 'today' | 'week' | 'month'
|
||||
) => {
|
||||
navigation.navigate('AIPredictionStats', { timeRange });
|
||||
};
|
||||
|
||||
/**
|
||||
* Go Back to AI Prediction List
|
||||
*
|
||||
* Purpose: Navigate back to AI prediction list screen
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
*/
|
||||
export const goBackToAIPredictionList = (navigation: any) => {
|
||||
navigation.navigate('AIPredictionList');
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset to AI Prediction List
|
||||
*
|
||||
* Purpose: Reset navigation stack to AI prediction list
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
*/
|
||||
export const resetToAIPredictionList = (navigation: any) => {
|
||||
navigation.dispatch(
|
||||
CommonActions.reset({
|
||||
index: 0,
|
||||
routes: [{ name: 'AIPredictionList' }],
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Can Go Back
|
||||
*
|
||||
* Purpose: Check if navigation can go back
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @returns Boolean indicating if can go back
|
||||
*/
|
||||
export const canGoBack = (navigation: any): boolean => {
|
||||
return navigation.canGoBack();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Current Route Name
|
||||
*
|
||||
* Purpose: Get the current route name
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @returns Current route name or undefined
|
||||
*/
|
||||
export const getCurrentRouteName = (navigation: any): string | undefined => {
|
||||
return navigation.getCurrentRoute()?.name;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Current Route Params
|
||||
*
|
||||
* Purpose: Get the current route parameters
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @returns Current route params or undefined
|
||||
*/
|
||||
export const getCurrentRouteParams = (navigation: any): any => {
|
||||
return navigation.getCurrentRoute()?.params;
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate with Replace
|
||||
*
|
||||
* Purpose: Navigate to a screen by replacing the current one
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @param routeName - Route name to navigate to
|
||||
* @param params - Optional route parameters
|
||||
*/
|
||||
export const navigateWithReplace = (
|
||||
navigation: any,
|
||||
routeName: keyof AIPredictionStackParamList,
|
||||
params?: any
|
||||
) => {
|
||||
navigation.replace(routeName, params);
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigate with Push
|
||||
*
|
||||
* Purpose: Navigate to a screen by pushing it onto the stack
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @param routeName - Route name to navigate to
|
||||
* @param params - Optional route parameters
|
||||
*/
|
||||
export const navigateWithPush = (
|
||||
navigation: any,
|
||||
routeName: keyof AIPredictionStackParamList,
|
||||
params?: any
|
||||
) => {
|
||||
navigation.push(routeName, params);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pop Navigation Stack
|
||||
*
|
||||
* Purpose: Pop the specified number of screens from the stack
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @param count - Number of screens to pop (default: 1)
|
||||
*/
|
||||
export const popNavigationStack = (navigation: any, count: number = 1) => {
|
||||
navigation.pop(count);
|
||||
};
|
||||
|
||||
/**
|
||||
* Pop to Top
|
||||
*
|
||||
* Purpose: Pop to the top of the navigation stack
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
*/
|
||||
export const popToTop = (navigation: any) => {
|
||||
navigation.popToTop();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set Navigation Params
|
||||
*
|
||||
* Purpose: Set parameters for the current screen
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @param params - Parameters to set
|
||||
*/
|
||||
export const setNavigationParams = (navigation: any, params: any) => {
|
||||
navigation.setParams(params);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add Navigation Listener
|
||||
*
|
||||
* Purpose: Add a navigation event listener
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @param eventName - Event name to listen for
|
||||
* @param callback - Callback function
|
||||
* @returns Unsubscribe function
|
||||
*/
|
||||
export const addNavigationListener = (
|
||||
navigation: any,
|
||||
eventName: string,
|
||||
callback: (e: any) => void
|
||||
) => {
|
||||
return navigation.addListener(eventName, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove Navigation Listener
|
||||
*
|
||||
* Purpose: Remove a navigation event listener
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @param eventName - Event name
|
||||
* @param callback - Callback function
|
||||
*/
|
||||
export const removeNavigationListener = (
|
||||
navigation: any,
|
||||
eventName: string,
|
||||
callback: (e: any) => void
|
||||
) => {
|
||||
navigation.removeListener(eventName, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if Screen is Focused
|
||||
*
|
||||
* Purpose: Check if the current screen is focused
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @returns Boolean indicating if screen is focused
|
||||
*/
|
||||
export const isScreenFocused = (navigation: any): boolean => {
|
||||
return navigation.isFocused();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Navigation State
|
||||
*
|
||||
* Purpose: Get the current navigation state
|
||||
*
|
||||
* @param navigation - Navigation object
|
||||
* @returns Navigation state
|
||||
*/
|
||||
export const getNavigationState = (navigation: any) => {
|
||||
return navigation.getState();
|
||||
};
|
||||
|
||||
/*
|
||||
* End of File: navigationUtils.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
410
app/modules/AIPrediction/redux/aiPredictionSelectors.ts
Normal file
410
app/modules/AIPrediction/redux/aiPredictionSelectors.ts
Normal file
@ -0,0 +1,410 @@
|
||||
/*
|
||||
* File: aiPredictionSelectors.ts
|
||||
* Description: Redux selectors for AI Prediction state
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { RootState } from '../../../store';
|
||||
import { AIPredictionCase } from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// BASE SELECTORS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Select AI Prediction State
|
||||
*
|
||||
* Purpose: Get the entire AI prediction state
|
||||
*/
|
||||
export const selectAIPredictionState = (state: RootState) => state.aiPrediction;
|
||||
|
||||
/**
|
||||
* Select Prediction Cases
|
||||
*
|
||||
* Purpose: Get all AI prediction cases
|
||||
*/
|
||||
export const selectPredictionCases = (state: RootState) => state.aiPrediction.predictionCases;
|
||||
|
||||
/**
|
||||
* Select Current Case
|
||||
*
|
||||
* Purpose: Get the currently selected AI prediction case
|
||||
*/
|
||||
export const selectCurrentCase = (state: RootState) => state.aiPrediction.currentCase;
|
||||
|
||||
/**
|
||||
* Select Loading State
|
||||
*
|
||||
* Purpose: Get the loading state for AI predictions
|
||||
*/
|
||||
export const selectIsLoading = (state: RootState) => state.aiPrediction.isLoading;
|
||||
|
||||
/**
|
||||
* Select Loading Case Details State
|
||||
*
|
||||
* Purpose: Get the loading state for case details
|
||||
*/
|
||||
export const selectIsLoadingCaseDetails = (state: RootState) => state.aiPrediction.isLoadingCaseDetails;
|
||||
|
||||
/**
|
||||
* Select Error
|
||||
*
|
||||
* Purpose: Get the current error message
|
||||
*/
|
||||
export const selectError = (state: RootState) => state.aiPrediction.error;
|
||||
|
||||
/**
|
||||
* Select Search Query
|
||||
*
|
||||
* Purpose: Get the current search query
|
||||
*/
|
||||
export const selectSearchQuery = (state: RootState) => state.aiPrediction.searchQuery;
|
||||
|
||||
/**
|
||||
* Select Filter States
|
||||
*
|
||||
* Purpose: Get all filter states
|
||||
*/
|
||||
export const selectUrgencyFilter = (state: RootState) => state.aiPrediction.selectedUrgencyFilter;
|
||||
export const selectSeverityFilter = (state: RootState) => state.aiPrediction.selectedSeverityFilter;
|
||||
export const selectCategoryFilter = (state: RootState) => state.aiPrediction.selectedCategoryFilter;
|
||||
|
||||
/**
|
||||
* Select Sort Options
|
||||
*
|
||||
* Purpose: Get current sort configuration
|
||||
*/
|
||||
export const selectSortBy = (state: RootState) => state.aiPrediction.sortBy;
|
||||
export const selectSortOrder = (state: RootState) => state.aiPrediction.sortOrder;
|
||||
|
||||
/**
|
||||
* Select Pagination
|
||||
*
|
||||
* Purpose: Get pagination configuration
|
||||
*/
|
||||
export const selectCurrentPage = (state: RootState) => state.aiPrediction.currentPage;
|
||||
export const selectItemsPerPage = (state: RootState) => state.aiPrediction.itemsPerPage;
|
||||
export const selectTotalItems = (state: RootState) => state.aiPrediction.totalItems;
|
||||
|
||||
/**
|
||||
* Select UI State
|
||||
*
|
||||
* Purpose: Get UI state flags
|
||||
*/
|
||||
export const selectShowFilters = (state: RootState) => state.aiPrediction.showFilters;
|
||||
export const selectSelectedCaseIds = (state: RootState) => state.aiPrediction.selectedCaseIds;
|
||||
|
||||
// ============================================================================
|
||||
// COMPUTED SELECTORS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Select Filtered and Sorted Cases
|
||||
*
|
||||
* Purpose: Get AI prediction cases filtered and sorted based on current settings
|
||||
*/
|
||||
export const selectFilteredAndSortedCases = createSelector(
|
||||
[
|
||||
selectPredictionCases,
|
||||
selectSearchQuery,
|
||||
selectUrgencyFilter,
|
||||
selectSeverityFilter,
|
||||
selectCategoryFilter,
|
||||
selectSortBy,
|
||||
selectSortOrder,
|
||||
],
|
||||
(cases, searchQuery, urgencyFilter, severityFilter, categoryFilter, sortBy, sortOrder) => {
|
||||
let filteredCases = [...cases];
|
||||
|
||||
// Apply search filter
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
filteredCases = filteredCases.filter(case_ =>
|
||||
case_.patid.toLowerCase().includes(query) ||
|
||||
case_.prediction.label.toLowerCase().includes(query) ||
|
||||
case_.prediction.anatomical_location.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Apply urgency filter
|
||||
if (urgencyFilter !== 'all') {
|
||||
filteredCases = filteredCases.filter(case_ =>
|
||||
case_.prediction.clinical_urgency === urgencyFilter
|
||||
);
|
||||
}
|
||||
|
||||
// Apply severity filter
|
||||
if (severityFilter !== 'all') {
|
||||
filteredCases = filteredCases.filter(case_ =>
|
||||
case_.prediction.primary_severity === severityFilter
|
||||
);
|
||||
}
|
||||
|
||||
// Apply category filter
|
||||
if (categoryFilter !== 'all') {
|
||||
filteredCases = filteredCases.filter(case_ =>
|
||||
case_.prediction.finding_category === categoryFilter
|
||||
);
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
filteredCases.sort((a, b) => {
|
||||
let comparison = 0;
|
||||
|
||||
switch (sortBy) {
|
||||
case 'date':
|
||||
comparison = new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime();
|
||||
break;
|
||||
case 'urgency':
|
||||
const urgencyOrder = { emergency: 5, urgent: 4, moderate: 3, low: 2, routine: 1 };
|
||||
comparison = (urgencyOrder[a.prediction.clinical_urgency as keyof typeof urgencyOrder] || 0) -
|
||||
(urgencyOrder[b.prediction.clinical_urgency as keyof typeof urgencyOrder] || 0);
|
||||
break;
|
||||
case 'confidence':
|
||||
comparison = a.prediction.confidence_score - b.prediction.confidence_score;
|
||||
break;
|
||||
case 'severity':
|
||||
const severityOrder = { high: 4, medium: 3, low: 2, none: 1 };
|
||||
comparison = (severityOrder[a.prediction.primary_severity as keyof typeof severityOrder] || 0) -
|
||||
(severityOrder[b.prediction.primary_severity as keyof typeof severityOrder] || 0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return sortOrder === 'desc' ? -comparison : comparison;
|
||||
});
|
||||
|
||||
return filteredCases;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Paginated Cases
|
||||
*
|
||||
* Purpose: Get the current page of filtered and sorted cases
|
||||
*/
|
||||
export const selectPaginatedCases = createSelector(
|
||||
[selectFilteredAndSortedCases, selectCurrentPage, selectItemsPerPage],
|
||||
(filteredCases, currentPage, itemsPerPage) => {
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
return filteredCases.slice(startIndex, endIndex);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Critical Cases
|
||||
*
|
||||
* Purpose: Get cases marked as critical or emergency
|
||||
*/
|
||||
export const selectCriticalCases = createSelector(
|
||||
[selectPredictionCases],
|
||||
(cases) => cases.filter(case_ =>
|
||||
case_.prediction.clinical_urgency === 'emergency' ||
|
||||
case_.prediction.clinical_urgency === 'urgent' ||
|
||||
case_.prediction.primary_severity === 'high' ||
|
||||
case_.priority === 'critical'
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Pending Cases
|
||||
*
|
||||
* Purpose: Get cases pending review
|
||||
*/
|
||||
export const selectPendingCases = createSelector(
|
||||
[selectPredictionCases],
|
||||
(cases) => cases.filter(case_ => case_.review_status === 'pending')
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Reviewed Cases
|
||||
*
|
||||
* Purpose: Get cases that have been reviewed
|
||||
*/
|
||||
export const selectReviewedCases = createSelector(
|
||||
[selectPredictionCases],
|
||||
(cases) => cases.filter(case_ =>
|
||||
case_.review_status === 'reviewed' ||
|
||||
case_.review_status === 'confirmed' ||
|
||||
case_.review_status === 'disputed'
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Cases by Urgency
|
||||
*
|
||||
* Purpose: Group cases by urgency level
|
||||
*/
|
||||
export const selectCasesByUrgency = createSelector(
|
||||
[selectPredictionCases],
|
||||
(cases) => {
|
||||
const grouped = {
|
||||
emergency: [] as AIPredictionCase[],
|
||||
urgent: [] as AIPredictionCase[],
|
||||
moderate: [] as AIPredictionCase[],
|
||||
low: [] as AIPredictionCase[],
|
||||
routine: [] as AIPredictionCase[],
|
||||
};
|
||||
|
||||
cases.forEach(case_ => {
|
||||
const urgency = case_.prediction.clinical_urgency as keyof typeof grouped;
|
||||
if (grouped[urgency]) {
|
||||
grouped[urgency].push(case_);
|
||||
}
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Cases Statistics
|
||||
*
|
||||
* Purpose: Get statistical overview of cases
|
||||
*/
|
||||
export const selectCasesStatistics = createSelector(
|
||||
[selectPredictionCases],
|
||||
(cases) => {
|
||||
const total = cases.length;
|
||||
const critical = cases.filter(c =>
|
||||
c.prediction.clinical_urgency === 'emergency' ||
|
||||
c.prediction.clinical_urgency === 'urgent'
|
||||
).length;
|
||||
const pending = cases.filter(c => c.review_status === 'pending').length;
|
||||
const reviewed = cases.filter(c =>
|
||||
c.review_status === 'reviewed' ||
|
||||
c.review_status === 'confirmed'
|
||||
).length;
|
||||
const averageConfidence = total > 0
|
||||
? cases.reduce((sum, c) => sum + c.prediction.confidence_score, 0) / total
|
||||
: 0;
|
||||
|
||||
return {
|
||||
total,
|
||||
critical,
|
||||
pending,
|
||||
reviewed,
|
||||
averageConfidence: Math.round(averageConfidence * 1000) / 1000, // Round to 3 decimal places
|
||||
reviewProgress: total > 0 ? Math.round((reviewed / total) * 100) : 0,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Filter Counts
|
||||
*
|
||||
* Purpose: Get counts for each filter option
|
||||
*/
|
||||
export const selectFilterCounts = createSelector(
|
||||
[selectPredictionCases],
|
||||
(cases) => {
|
||||
const urgencyCounts = {
|
||||
all: cases.length,
|
||||
emergency: 0,
|
||||
urgent: 0,
|
||||
moderate: 0,
|
||||
low: 0,
|
||||
routine: 0,
|
||||
};
|
||||
|
||||
const severityCounts = {
|
||||
all: cases.length,
|
||||
high: 0,
|
||||
medium: 0,
|
||||
low: 0,
|
||||
none: 0,
|
||||
};
|
||||
|
||||
const categoryCounts = {
|
||||
all: cases.length,
|
||||
normal: 0,
|
||||
abnormal: 0,
|
||||
critical: 0,
|
||||
warning: 0,
|
||||
unknown: 0,
|
||||
};
|
||||
|
||||
cases.forEach(case_ => {
|
||||
// Count urgency
|
||||
const urgency = case_.prediction.clinical_urgency as keyof typeof urgencyCounts;
|
||||
if (urgencyCounts[urgency] !== undefined) {
|
||||
urgencyCounts[urgency]++;
|
||||
}
|
||||
|
||||
// Count severity
|
||||
const severity = case_.prediction.primary_severity as keyof typeof severityCounts;
|
||||
if (severityCounts[severity] !== undefined) {
|
||||
severityCounts[severity]++;
|
||||
}
|
||||
|
||||
// Count category
|
||||
const category = case_.prediction.finding_category as keyof typeof categoryCounts;
|
||||
if (categoryCounts[category] !== undefined) {
|
||||
categoryCounts[category]++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
urgency: urgencyCounts,
|
||||
severity: severityCounts,
|
||||
category: categoryCounts,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Total Pages
|
||||
*
|
||||
* Purpose: Calculate total number of pages based on filtered results
|
||||
*/
|
||||
export const selectTotalPages = createSelector(
|
||||
[selectFilteredAndSortedCases, selectItemsPerPage],
|
||||
(filteredCases, itemsPerPage) => Math.ceil(filteredCases.length / itemsPerPage)
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Has Previous Page
|
||||
*
|
||||
* Purpose: Check if there's a previous page available
|
||||
*/
|
||||
export const selectHasPreviousPage = createSelector(
|
||||
[selectCurrentPage],
|
||||
(currentPage) => currentPage > 1
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Has Next Page
|
||||
*
|
||||
* Purpose: Check if there's a next page available
|
||||
*/
|
||||
export const selectHasNextPage = createSelector(
|
||||
[selectCurrentPage, selectTotalPages],
|
||||
(currentPage, totalPages) => currentPage < totalPages
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Active Filters Count
|
||||
*
|
||||
* Purpose: Count how many filters are currently active
|
||||
*/
|
||||
export const selectActiveFiltersCount = createSelector(
|
||||
[selectSearchQuery, selectUrgencyFilter, selectSeverityFilter, selectCategoryFilter],
|
||||
(searchQuery, urgencyFilter, severityFilter, categoryFilter) => {
|
||||
let count = 0;
|
||||
if (searchQuery.trim()) count++;
|
||||
if (urgencyFilter !== 'all') count++;
|
||||
if (severityFilter !== 'all') count++;
|
||||
if (categoryFilter !== 'all') count++;
|
||||
return count;
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
* End of File: aiPredictionSelectors.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
621
app/modules/AIPrediction/redux/aiPredictionSlice.ts
Normal file
621
app/modules/AIPrediction/redux/aiPredictionSlice.ts
Normal file
@ -0,0 +1,621 @@
|
||||
/*
|
||||
* File: aiPredictionSlice.ts
|
||||
* Description: Redux slice for AI Prediction state management
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import {
|
||||
AIPredictionCase,
|
||||
AIPredictionState,
|
||||
AIPredictionStats,
|
||||
AIPredictionAPIResponse
|
||||
} from '../types';
|
||||
import { aiPredictionAPI } from '../services';
|
||||
|
||||
// ============================================================================
|
||||
// ASYNC THUNKS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch AI Predictions Async Thunk
|
||||
*
|
||||
* Purpose: Fetch AI prediction results from API
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @param params - Optional query parameters for filtering
|
||||
* @returns Promise with AI prediction data or error
|
||||
*/
|
||||
export const fetchAIPredictions = createAsyncThunk(
|
||||
'aiPrediction/fetchAIPredictions',
|
||||
async (payload: {
|
||||
token: string;
|
||||
params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
urgency?: string;
|
||||
severity?: string;
|
||||
category?: string;
|
||||
search?: string;
|
||||
}
|
||||
}, { rejectWithValue }) => {
|
||||
try {
|
||||
const response: any = await aiPredictionAPI.getAllPredictions(payload.token, payload.params);
|
||||
console.log('AI predictions response:', response);
|
||||
|
||||
if (response.ok && response.data && response.data.success) {
|
||||
// Add additional metadata to each case for UI purposes
|
||||
const enhancedCases = response.data.data.map((aiCase: AIPredictionCase) => ({
|
||||
...aiCase,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
review_status: 'pending' as const,
|
||||
priority: getPriorityFromPrediction(aiCase.prediction)
|
||||
}));
|
||||
|
||||
console.log('Enhanced AI prediction cases:', enhancedCases);
|
||||
return {
|
||||
cases: enhancedCases as AIPredictionCase[],
|
||||
total: response.data.total || enhancedCases.length,
|
||||
page: response.data.page || 1,
|
||||
limit: response.data.limit || 20
|
||||
};
|
||||
} else {
|
||||
// Fallback to mock data for development
|
||||
const mockData = generateMockAIPredictions();
|
||||
return {
|
||||
cases: mockData,
|
||||
total: mockData.length,
|
||||
page: 1,
|
||||
limit: 20
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch AI predictions error:', error);
|
||||
return rejectWithValue(error.message || 'Failed to fetch AI predictions.');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetch AI Prediction Case Details Async Thunk
|
||||
*
|
||||
* Purpose: Fetch detailed information for a specific AI prediction case
|
||||
*
|
||||
* @param caseId - AI prediction case ID
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with case details or error
|
||||
*/
|
||||
export const fetchAIPredictionDetails = createAsyncThunk(
|
||||
'aiPrediction/fetchAIPredictionDetails',
|
||||
async (payload: { caseId: string; token: string }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response: any = await aiPredictionAPI.getCaseDetails(payload.caseId, payload.token);
|
||||
|
||||
if (response.ok && response.data) {
|
||||
return response.data as AIPredictionCase;
|
||||
} else {
|
||||
// Fallback to mock data
|
||||
const mockCase = generateMockAIPredictions().find(c => c.patid === payload.caseId);
|
||||
if (mockCase) {
|
||||
return mockCase;
|
||||
}
|
||||
throw new Error('Case not found');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch AI prediction details error:', error);
|
||||
return rejectWithValue(error.message || 'Failed to fetch case details.');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Update Case Review Async Thunk
|
||||
*
|
||||
* Purpose: Update review status of an AI prediction case
|
||||
*
|
||||
* @param caseId - Case ID to update
|
||||
* @param reviewData - Review data
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with updated case or error
|
||||
*/
|
||||
export const updateCaseReview = createAsyncThunk(
|
||||
'aiPrediction/updateCaseReview',
|
||||
async (payload: {
|
||||
caseId: string;
|
||||
reviewData: {
|
||||
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
|
||||
reviewed_by?: string;
|
||||
review_notes?: string;
|
||||
priority?: 'critical' | 'high' | 'medium' | 'low';
|
||||
};
|
||||
token: string;
|
||||
}, { rejectWithValue }) => {
|
||||
try {
|
||||
const response: any = await aiPredictionAPI.updateCaseReview(
|
||||
payload.caseId,
|
||||
payload.reviewData,
|
||||
payload.token
|
||||
);
|
||||
|
||||
if (response.ok && response.data) {
|
||||
return {
|
||||
caseId: payload.caseId,
|
||||
...payload.reviewData,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
} else {
|
||||
throw new Error('Failed to update case review');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Update case review error:', error);
|
||||
return rejectWithValue(error.message || 'Failed to update case review.');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetch AI Prediction Statistics Async Thunk
|
||||
*
|
||||
* Purpose: Fetch statistics for AI predictions dashboard
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @param timeRange - Time range filter
|
||||
* @returns Promise with statistics data or error
|
||||
*/
|
||||
export const fetchAIPredictionStats = createAsyncThunk(
|
||||
'aiPrediction/fetchAIPredictionStats',
|
||||
async (payload: { token: string; timeRange?: 'today' | 'week' | 'month' }, { rejectWithValue }) => {
|
||||
try {
|
||||
const response: any = await aiPredictionAPI.getPredictionStats(payload.token, payload.timeRange);
|
||||
|
||||
if (response.ok && response.data) {
|
||||
return response.data as AIPredictionStats;
|
||||
} else {
|
||||
// Fallback to mock stats
|
||||
return generateMockStats();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch AI prediction stats error:', error);
|
||||
return rejectWithValue(error.message || 'Failed to fetch statistics.');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get Priority from AI Prediction
|
||||
*
|
||||
* Purpose: Determine case priority based on AI prediction results
|
||||
*/
|
||||
function getPriorityFromPrediction(prediction: any): 'critical' | 'high' | 'medium' | 'low' {
|
||||
if (prediction.clinical_urgency === 'emergency' || prediction.primary_severity === 'high') {
|
||||
return 'critical';
|
||||
}
|
||||
if (prediction.clinical_urgency === 'urgent' || prediction.primary_severity === 'medium') {
|
||||
return 'high';
|
||||
}
|
||||
if (prediction.clinical_urgency === 'moderate' || prediction.primary_severity === 'low') {
|
||||
return 'medium';
|
||||
}
|
||||
return 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Mock AI Predictions
|
||||
*
|
||||
* Purpose: Generate mock data for development and testing
|
||||
*/
|
||||
function generateMockAIPredictions(): AIPredictionCase[] {
|
||||
return [
|
||||
{
|
||||
patid: "demogw05-08-2017",
|
||||
hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2",
|
||||
prediction: {
|
||||
label: "midline shift",
|
||||
finding_type: "pathology",
|
||||
clinical_urgency: "urgent",
|
||||
confidence_score: 0.996,
|
||||
finding_category: "abnormal",
|
||||
primary_severity: "high",
|
||||
anatomical_location: "brain"
|
||||
},
|
||||
created_at: "2024-01-15T10:30:00Z",
|
||||
updated_at: "2024-01-15T10:30:00Z",
|
||||
review_status: "pending",
|
||||
priority: "critical"
|
||||
},
|
||||
{
|
||||
patid: "demo-patient-002",
|
||||
hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2",
|
||||
prediction: {
|
||||
label: "normal brain",
|
||||
finding_type: "no_pathology",
|
||||
clinical_urgency: "routine",
|
||||
confidence_score: 0.892,
|
||||
finding_category: "normal",
|
||||
primary_severity: "none",
|
||||
anatomical_location: "not_applicable"
|
||||
},
|
||||
created_at: "2024-01-15T09:15:00Z",
|
||||
updated_at: "2024-01-15T09:15:00Z",
|
||||
review_status: "reviewed",
|
||||
priority: "low"
|
||||
},
|
||||
{
|
||||
patid: "demo-patient-003",
|
||||
hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2",
|
||||
prediction: {
|
||||
label: "hemorrhage",
|
||||
finding_type: "pathology",
|
||||
clinical_urgency: "emergency",
|
||||
confidence_score: 0.945,
|
||||
finding_category: "critical",
|
||||
primary_severity: "high",
|
||||
anatomical_location: "temporal lobe"
|
||||
},
|
||||
created_at: "2024-01-15T11:45:00Z",
|
||||
updated_at: "2024-01-15T11:45:00Z",
|
||||
review_status: "confirmed",
|
||||
priority: "critical"
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Mock Statistics
|
||||
*
|
||||
* Purpose: Generate mock statistics for development
|
||||
*/
|
||||
function generateMockStats(): AIPredictionStats {
|
||||
return {
|
||||
totalCases: 156,
|
||||
criticalCases: 23,
|
||||
urgentCases: 45,
|
||||
reviewedCases: 89,
|
||||
pendingCases: 67,
|
||||
averageConfidence: 0.887,
|
||||
todaysCases: 12,
|
||||
weeklyTrend: 15.4
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INITIAL STATE
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Initial AI Prediction State
|
||||
*
|
||||
* Purpose: Define the initial state for AI predictions
|
||||
*
|
||||
* Features:
|
||||
* - Prediction cases list and management
|
||||
* - Current case details
|
||||
* - Loading states for async operations
|
||||
* - Error handling and messages
|
||||
* - Search and filtering
|
||||
* - Pagination support
|
||||
* - Cache management
|
||||
*/
|
||||
const initialState: AIPredictionState = {
|
||||
// Prediction data
|
||||
predictionCases: [],
|
||||
currentCase: null,
|
||||
|
||||
// Loading states
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
isLoadingCaseDetails: false,
|
||||
|
||||
// Error handling
|
||||
error: null,
|
||||
|
||||
// Search and filtering
|
||||
searchQuery: '',
|
||||
selectedUrgencyFilter: 'all',
|
||||
selectedSeverityFilter: 'all',
|
||||
selectedCategoryFilter: 'all',
|
||||
sortBy: 'date',
|
||||
sortOrder: 'desc',
|
||||
|
||||
// Pagination
|
||||
currentPage: 1,
|
||||
itemsPerPage: 20,
|
||||
totalItems: 0,
|
||||
|
||||
// Cache management
|
||||
lastUpdated: null,
|
||||
cacheExpiry: null,
|
||||
|
||||
// UI state
|
||||
showFilters: false,
|
||||
selectedCaseIds: [],
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// AI PREDICTION SLICE
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AI Prediction Slice
|
||||
*
|
||||
* Purpose: Redux slice for AI prediction state management
|
||||
*
|
||||
* Features:
|
||||
* - AI prediction data management
|
||||
* - Search and filtering
|
||||
* - Case review management
|
||||
* - Pagination
|
||||
* - Caching
|
||||
* - Error handling
|
||||
* - Loading states
|
||||
*/
|
||||
const aiPredictionSlice = createSlice({
|
||||
name: 'aiPrediction',
|
||||
initialState,
|
||||
reducers: {
|
||||
/**
|
||||
* Clear Error Action
|
||||
*
|
||||
* Purpose: Clear AI prediction errors
|
||||
*/
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Search Query Action
|
||||
*
|
||||
* Purpose: Set search query for AI predictions
|
||||
*/
|
||||
setSearchQuery: (state, action: PayloadAction<string>) => {
|
||||
state.searchQuery = action.payload;
|
||||
state.currentPage = 1; // Reset to first page when searching
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Urgency Filter Action
|
||||
*
|
||||
* Purpose: Set urgency filter for AI predictions
|
||||
*/
|
||||
setUrgencyFilter: (state, action: PayloadAction<AIPredictionState['selectedUrgencyFilter']>) => {
|
||||
state.selectedUrgencyFilter = action.payload;
|
||||
state.currentPage = 1; // Reset to first page when filtering
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Severity Filter Action
|
||||
*
|
||||
* Purpose: Set severity filter for AI predictions
|
||||
*/
|
||||
setSeverityFilter: (state, action: PayloadAction<AIPredictionState['selectedSeverityFilter']>) => {
|
||||
state.selectedSeverityFilter = action.payload;
|
||||
state.currentPage = 1; // Reset to first page when filtering
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Category Filter Action
|
||||
*
|
||||
* Purpose: Set category filter for AI predictions
|
||||
*/
|
||||
setCategoryFilter: (state, action: PayloadAction<AIPredictionState['selectedCategoryFilter']>) => {
|
||||
state.selectedCategoryFilter = action.payload;
|
||||
state.currentPage = 1; // Reset to first page when filtering
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Sort Action
|
||||
*
|
||||
* Purpose: Set sort options for AI predictions
|
||||
*/
|
||||
setSort: (state, action: PayloadAction<{ by: 'date' | 'urgency' | 'confidence' | 'severity'; order: 'asc' | 'desc' }>) => {
|
||||
state.sortBy = action.payload.by;
|
||||
state.sortOrder = action.payload.order;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Current Page Action
|
||||
*
|
||||
* Purpose: Set current page for pagination
|
||||
*/
|
||||
setCurrentPage: (state, action: PayloadAction<number>) => {
|
||||
state.currentPage = action.payload;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Items Per Page Action
|
||||
*
|
||||
* Purpose: Set items per page for pagination
|
||||
*/
|
||||
setItemsPerPage: (state, action: PayloadAction<number>) => {
|
||||
state.itemsPerPage = action.payload;
|
||||
state.currentPage = 1; // Reset to first page when changing items per page
|
||||
},
|
||||
|
||||
/**
|
||||
* Set Current Case Action
|
||||
*
|
||||
* Purpose: Set the currently selected AI prediction case
|
||||
*/
|
||||
setCurrentCase: (state, action: PayloadAction<AIPredictionCase | null>) => {
|
||||
state.currentCase = action.payload;
|
||||
},
|
||||
|
||||
/**
|
||||
* Update Case in List Action
|
||||
*
|
||||
* Purpose: Update an AI prediction case in the list
|
||||
*/
|
||||
updateCaseInList: (state, action: PayloadAction<AIPredictionCase>) => {
|
||||
const index = state.predictionCases.findIndex(case_ => case_.patid === action.payload.patid);
|
||||
if (index !== -1) {
|
||||
state.predictionCases[index] = action.payload;
|
||||
}
|
||||
|
||||
// Update current case if it's the same case
|
||||
if (state.currentCase && state.currentCase.patid === action.payload.patid) {
|
||||
state.currentCase = action.payload;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle Show Filters Action
|
||||
*
|
||||
* Purpose: Toggle the display of filter options
|
||||
*/
|
||||
toggleShowFilters: (state) => {
|
||||
state.showFilters = !state.showFilters;
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear All Filters Action
|
||||
*
|
||||
* Purpose: Reset all filters to default values
|
||||
*/
|
||||
clearAllFilters: (state) => {
|
||||
state.searchQuery = '';
|
||||
state.selectedUrgencyFilter = 'all';
|
||||
state.selectedSeverityFilter = 'all';
|
||||
state.selectedCategoryFilter = 'all';
|
||||
state.currentPage = 1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Select Case Action
|
||||
*
|
||||
* Purpose: Add/remove case from selected cases
|
||||
*/
|
||||
toggleCaseSelection: (state, action: PayloadAction<string>) => {
|
||||
const caseId = action.payload;
|
||||
const index = state.selectedCaseIds.indexOf(caseId);
|
||||
|
||||
if (index === -1) {
|
||||
state.selectedCaseIds.push(caseId);
|
||||
} else {
|
||||
state.selectedCaseIds.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear Selected Cases Action
|
||||
*
|
||||
* Purpose: Clear all selected cases
|
||||
*/
|
||||
clearSelectedCases: (state) => {
|
||||
state.selectedCaseIds = [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear Cache Action
|
||||
*
|
||||
* Purpose: Clear AI prediction data cache
|
||||
*/
|
||||
clearCache: (state) => {
|
||||
state.predictionCases = [];
|
||||
state.currentCase = null;
|
||||
state.lastUpdated = null;
|
||||
state.cacheExpiry = null;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Fetch AI Predictions
|
||||
builder
|
||||
.addCase(fetchAIPredictions.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchAIPredictions.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.predictionCases = action.payload.cases;
|
||||
state.totalItems = action.payload.total;
|
||||
state.lastUpdated = new Date().toLocaleString();
|
||||
state.cacheExpiry = new Date(Date.now() + 5 * 60 * 1000).toLocaleString(); // 5 minutes
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchAIPredictions.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// Fetch AI Prediction Details
|
||||
builder
|
||||
.addCase(fetchAIPredictionDetails.pending, (state) => {
|
||||
state.isLoadingCaseDetails = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchAIPredictionDetails.fulfilled, (state, action) => {
|
||||
state.isLoadingCaseDetails = false;
|
||||
state.currentCase = action.payload;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchAIPredictionDetails.rejected, (state, action) => {
|
||||
state.isLoadingCaseDetails = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// Update Case Review
|
||||
builder
|
||||
.addCase(updateCaseReview.fulfilled, (state, action) => {
|
||||
// Update case in list
|
||||
const index = state.predictionCases.findIndex(case_ => case_.patid === action.payload.caseId);
|
||||
if (index !== -1) {
|
||||
state.predictionCases[index] = {
|
||||
...state.predictionCases[index],
|
||||
review_status: action.payload.review_status,
|
||||
reviewed_by: action.payload.reviewed_by,
|
||||
priority: action.payload.priority,
|
||||
updated_at: action.payload.updated_at
|
||||
};
|
||||
}
|
||||
|
||||
// Update current case if it's the same case
|
||||
if (state.currentCase && state.currentCase.patid === action.payload.caseId) {
|
||||
state.currentCase = {
|
||||
...state.currentCase,
|
||||
review_status: action.payload.review_status,
|
||||
reviewed_by: action.payload.reviewed_by,
|
||||
priority: action.payload.priority,
|
||||
updated_at: action.payload.updated_at
|
||||
};
|
||||
}
|
||||
})
|
||||
.addCase(updateCaseReview.rejected, (state, action) => {
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// EXPORTS
|
||||
// ============================================================================
|
||||
|
||||
export const {
|
||||
clearError,
|
||||
setSearchQuery,
|
||||
setUrgencyFilter,
|
||||
setSeverityFilter,
|
||||
setCategoryFilter,
|
||||
setSort,
|
||||
setCurrentPage,
|
||||
setItemsPerPage,
|
||||
setCurrentCase,
|
||||
updateCaseInList,
|
||||
toggleShowFilters,
|
||||
clearAllFilters,
|
||||
toggleCaseSelection,
|
||||
clearSelectedCases,
|
||||
clearCache,
|
||||
} = aiPredictionSlice.actions;
|
||||
|
||||
export default aiPredictionSlice.reducer;
|
||||
|
||||
/*
|
||||
* End of File: aiPredictionSlice.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
15
app/modules/AIPrediction/redux/index.ts
Normal file
15
app/modules/AIPrediction/redux/index.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Redux exports for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export * from './aiPredictionSlice';
|
||||
export * from './aiPredictionSelectors';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
749
app/modules/AIPrediction/screens/AIPredictionsScreen.tsx
Normal file
749
app/modules/AIPrediction/screens/AIPredictionsScreen.tsx
Normal file
@ -0,0 +1,749 @@
|
||||
/*
|
||||
* File: AIPredictionsScreen.tsx
|
||||
* Description: Main AI Predictions screen with data rendering and management
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
StatusBar,
|
||||
SafeAreaView,
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
|
||||
// Import Redux actions and selectors
|
||||
import {
|
||||
fetchAIPredictions,
|
||||
setSearchQuery,
|
||||
setUrgencyFilter,
|
||||
setSeverityFilter,
|
||||
setCategoryFilter,
|
||||
setCurrentPage,
|
||||
clearAllFilters,
|
||||
toggleShowFilters,
|
||||
toggleCaseSelection,
|
||||
clearSelectedCases,
|
||||
updateCaseReview,
|
||||
} from '../redux';
|
||||
|
||||
import {
|
||||
selectPaginatedCases,
|
||||
selectIsLoading,
|
||||
selectError,
|
||||
selectSearchQuery,
|
||||
selectUrgencyFilter,
|
||||
selectSeverityFilter,
|
||||
selectCategoryFilter,
|
||||
selectShowFilters,
|
||||
selectSelectedCaseIds,
|
||||
selectCasesStatistics,
|
||||
selectFilterCounts,
|
||||
selectActiveFiltersCount,
|
||||
selectCurrentPage,
|
||||
selectTotalPages,
|
||||
selectHasNextPage,
|
||||
selectHasPreviousPage,
|
||||
} from '../redux';
|
||||
|
||||
// Import components
|
||||
import {
|
||||
AIPredictionCard,
|
||||
SearchBar,
|
||||
FilterTabs,
|
||||
LoadingState,
|
||||
EmptyState,
|
||||
StatsOverview,
|
||||
} from '../components';
|
||||
|
||||
// Import types
|
||||
import type { AIPredictionCase } from '../types';
|
||||
|
||||
// Import auth selector
|
||||
import { selectUser } from '../../Auth/redux/authSelectors';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface AIPredictionsScreenProps {
|
||||
navigation: any;
|
||||
route?: any;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AI PREDICTIONS SCREEN COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AIPredictionsScreen Component
|
||||
*
|
||||
* Purpose: Main screen for displaying and managing AI prediction cases
|
||||
*
|
||||
* Features:
|
||||
* - Comprehensive AI predictions list
|
||||
* - Real-time search and filtering
|
||||
* - Statistics overview dashboard
|
||||
* - Bulk case selection and actions
|
||||
* - Pull-to-refresh functionality
|
||||
* - Pagination support
|
||||
* - Review status management
|
||||
* - Modern card-based design
|
||||
* - Error handling and retry
|
||||
* - Loading states and empty states
|
||||
* - Accessibility support
|
||||
*/
|
||||
const AIPredictionsScreen: React.FC<AIPredictionsScreenProps> = ({ navigation }) => {
|
||||
// ============================================================================
|
||||
// REDUX STATE
|
||||
// ============================================================================
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// Auth state
|
||||
const user :any = useAppSelector(selectUser);
|
||||
|
||||
// AI Prediction state
|
||||
const cases = useAppSelector(selectPaginatedCases);
|
||||
const isLoading = useAppSelector(selectIsLoading);
|
||||
const error = useAppSelector(selectError);
|
||||
const searchQuery = useAppSelector(selectSearchQuery);
|
||||
const urgencyFilter = useAppSelector(selectUrgencyFilter);
|
||||
const severityFilter = useAppSelector(selectSeverityFilter);
|
||||
const categoryFilter = useAppSelector(selectCategoryFilter);
|
||||
const showFilters = useAppSelector(selectShowFilters);
|
||||
const selectedCaseIds = useAppSelector(selectSelectedCaseIds);
|
||||
const statistics = useAppSelector(selectCasesStatistics);
|
||||
const filterCounts = useAppSelector(selectFilterCounts);
|
||||
const activeFiltersCount = useAppSelector(selectActiveFiltersCount);
|
||||
const currentPage = useAppSelector(selectCurrentPage);
|
||||
const totalPages = useAppSelector(selectTotalPages);
|
||||
const hasNextPage = useAppSelector(selectHasNextPage);
|
||||
const hasPreviousPage = useAppSelector(selectHasPreviousPage);
|
||||
|
||||
// ============================================================================
|
||||
// LOCAL STATE
|
||||
// ============================================================================
|
||||
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [showStats, setShowStats] = useState(true);
|
||||
|
||||
// ============================================================================
|
||||
// EFFECTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load AI Predictions on Mount
|
||||
*
|
||||
* Purpose: Fetch AI predictions when component mounts
|
||||
*/
|
||||
console.log('user ===>', user);
|
||||
useEffect(() => {
|
||||
if (user?.access_token) {
|
||||
loadAIPredictions();
|
||||
}
|
||||
}, [user?.access_token]);
|
||||
|
||||
/**
|
||||
* Load AI Predictions on Filter Change
|
||||
*
|
||||
* Purpose: Reload data when filters change
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (user?.access_token) {
|
||||
loadAIPredictions();
|
||||
}
|
||||
}, [urgencyFilter, severityFilter, categoryFilter, searchQuery, currentPage]);
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Load AI Predictions
|
||||
*
|
||||
* Purpose: Fetch AI predictions from API
|
||||
*/
|
||||
const loadAIPredictions = useCallback(async () => {
|
||||
if (!user?.access_token) return;
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage,
|
||||
limit: 20,
|
||||
...(urgencyFilter !== 'all' && { urgency: urgencyFilter }),
|
||||
...(severityFilter !== 'all' && { severity: severityFilter }),
|
||||
...(categoryFilter !== 'all' && { category: categoryFilter }),
|
||||
...(searchQuery.trim() && { search: searchQuery.trim() }),
|
||||
};
|
||||
|
||||
await dispatch(fetchAIPredictions({
|
||||
token: user.access_token,
|
||||
params,
|
||||
})).unwrap();
|
||||
} catch (error) {
|
||||
console.error('Failed to load AI predictions:', error);
|
||||
// Error is handled by Redux state
|
||||
}
|
||||
}, [dispatch, user?.access_token, currentPage, urgencyFilter, severityFilter, categoryFilter, searchQuery]);
|
||||
|
||||
/**
|
||||
* Handle Refresh
|
||||
*
|
||||
* Purpose: Handle pull-to-refresh
|
||||
*/
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await loadAIPredictions();
|
||||
setRefreshing(false);
|
||||
}, [loadAIPredictions]);
|
||||
|
||||
/**
|
||||
* Handle Search
|
||||
*
|
||||
* Purpose: Handle search query change
|
||||
*/
|
||||
const handleSearch = useCallback((query: string) => {
|
||||
dispatch(setSearchQuery(query));
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Handle Filter Changes
|
||||
*
|
||||
* Purpose: Handle filter option changes
|
||||
*/
|
||||
const handleUrgencyFilterChange = useCallback((filter: typeof urgencyFilter) => {
|
||||
dispatch(setUrgencyFilter(filter));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSeverityFilterChange = useCallback((filter: typeof severityFilter) => {
|
||||
dispatch(setSeverityFilter(filter));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleCategoryFilterChange = useCallback((filter: typeof categoryFilter) => {
|
||||
dispatch(setCategoryFilter(filter));
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Handle Clear Filters
|
||||
*
|
||||
* Purpose: Clear all active filters
|
||||
*/
|
||||
const handleClearFilters = useCallback(() => {
|
||||
dispatch(clearAllFilters());
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Handle Toggle Filters
|
||||
*
|
||||
* Purpose: Toggle filter visibility
|
||||
*/
|
||||
const handleToggleFilters = useCallback(() => {
|
||||
dispatch(toggleShowFilters());
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Handle Case Press
|
||||
*
|
||||
* Purpose: Navigate to case details
|
||||
*/
|
||||
const handleCasePress = useCallback((predictionCase: AIPredictionCase) => {
|
||||
navigation.navigate('AIPredictionDetails', { caseId: predictionCase.patid });
|
||||
}, [navigation]);
|
||||
|
||||
/**
|
||||
* Handle Case Review
|
||||
*
|
||||
* Purpose: Handle case review action
|
||||
*/
|
||||
const handleCaseReview = useCallback(async (caseId: string) => {
|
||||
if (!user?.access_token) return;
|
||||
|
||||
try {
|
||||
await dispatch(updateCaseReview({
|
||||
caseId,
|
||||
reviewData: {
|
||||
review_status: 'reviewed',
|
||||
reviewed_by: user.name || user.email || 'Current User',
|
||||
},
|
||||
token: user.access_token,
|
||||
})).unwrap();
|
||||
|
||||
Alert.alert(
|
||||
'Review Updated',
|
||||
'Case has been marked as reviewed.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
} catch (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
'Failed to update case review. Please try again.',
|
||||
[{ text: 'OK' }]
|
||||
);
|
||||
}
|
||||
}, [dispatch, user]);
|
||||
|
||||
/**
|
||||
* Handle Case Selection
|
||||
*
|
||||
* Purpose: Handle case selection for bulk operations
|
||||
*/
|
||||
const handleCaseSelection = useCallback((caseId: string) => {
|
||||
dispatch(toggleCaseSelection(caseId));
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Handle Bulk Actions
|
||||
*
|
||||
* Purpose: Handle bulk actions on selected cases
|
||||
*/
|
||||
const handleBulkReview = useCallback(() => {
|
||||
if (selectedCaseIds.length === 0) return;
|
||||
|
||||
Alert.alert(
|
||||
'Bulk Review',
|
||||
`Mark ${selectedCaseIds.length} cases as reviewed?`,
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Confirm',
|
||||
onPress: async () => {
|
||||
// Implement bulk review logic here
|
||||
// For now, just clear selections
|
||||
dispatch(clearSelectedCases());
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
}, [selectedCaseIds, dispatch]);
|
||||
|
||||
/**
|
||||
* Handle Page Change
|
||||
*
|
||||
* Purpose: Handle pagination
|
||||
*/
|
||||
const handlePreviousPage = useCallback(() => {
|
||||
if (hasPreviousPage) {
|
||||
dispatch(setCurrentPage(currentPage - 1));
|
||||
}
|
||||
}, [dispatch, currentPage, hasPreviousPage]);
|
||||
|
||||
const handleNextPage = useCallback(() => {
|
||||
if (hasNextPage) {
|
||||
dispatch(setCurrentPage(currentPage + 1));
|
||||
}
|
||||
}, [dispatch, currentPage, hasNextPage]);
|
||||
|
||||
/**
|
||||
* Handle Stats Press
|
||||
*
|
||||
* Purpose: Handle statistics card press
|
||||
*/
|
||||
const handleStatsPress = useCallback((statType: string) => {
|
||||
// Navigate to detailed statistics or apply relevant filters
|
||||
switch (statType) {
|
||||
case 'critical':
|
||||
dispatch(setUrgencyFilter('emergency'));
|
||||
break;
|
||||
case 'urgent':
|
||||
dispatch(setUrgencyFilter('urgent'));
|
||||
break;
|
||||
case 'pending':
|
||||
// Filter for pending reviews
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Handle Retry
|
||||
*
|
||||
* Purpose: Handle retry after error
|
||||
*/
|
||||
const handleRetry = useCallback(() => {
|
||||
loadAIPredictions();
|
||||
}, [loadAIPredictions]);
|
||||
|
||||
// ============================================================================
|
||||
// RENDER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Render AI Prediction Case
|
||||
*
|
||||
* Purpose: Render individual AI prediction case card
|
||||
*/
|
||||
const renderPredictionCase = useCallback(({ item }: { item: AIPredictionCase }) => (
|
||||
<AIPredictionCard
|
||||
predictionCase={item}
|
||||
onPress={handleCasePress}
|
||||
onReview={handleCaseReview}
|
||||
isSelected={selectedCaseIds.includes(item.patid)}
|
||||
onToggleSelect={handleCaseSelection}
|
||||
showReviewButton={true}
|
||||
/>
|
||||
), [handleCasePress, handleCaseReview, selectedCaseIds, handleCaseSelection]);
|
||||
|
||||
/**
|
||||
* Render List Header
|
||||
*
|
||||
* Purpose: Render search, filters, and statistics
|
||||
*/
|
||||
const renderListHeader = useCallback(() => (
|
||||
<View>
|
||||
{/* Statistics Overview */}
|
||||
{showStats && (
|
||||
<StatsOverview
|
||||
stats={{
|
||||
totalCases: statistics.total,
|
||||
criticalCases: statistics.critical,
|
||||
urgentCases: 0, // Would need to be calculated from urgency filter
|
||||
reviewedCases: statistics.reviewed,
|
||||
pendingCases: statistics.pending,
|
||||
averageConfidence: statistics.averageConfidence,
|
||||
todaysCases: 0, // Would need to be calculated from today's data
|
||||
weeklyTrend: 12.5, // Mock data
|
||||
}}
|
||||
onStatsPress={handleStatsPress}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Search Bar */}
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
placeholder="Search by patient ID, finding, location..."
|
||||
/>
|
||||
|
||||
{/* Filter Controls */}
|
||||
<View style={styles.filterControls}>
|
||||
<TouchableOpacity
|
||||
style={[styles.filterToggle, showFilters && styles.filterToggleActive]}
|
||||
onPress={handleToggleFilters}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Toggle filters"
|
||||
>
|
||||
<Icon name="filter" size={18} color={showFilters ? theme.colors.background : theme.colors.primary} />
|
||||
<Text style={[styles.filterToggleText, showFilters && styles.filterToggleActiveText]}>
|
||||
Filters
|
||||
</Text>
|
||||
{activeFiltersCount > 0 && (
|
||||
<View style={styles.filterBadge}>
|
||||
<Text style={styles.filterBadgeText}>{activeFiltersCount}</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
{selectedCaseIds.length > 0 && (
|
||||
<TouchableOpacity
|
||||
style={styles.bulkActionButton}
|
||||
onPress={handleBulkReview}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`Bulk actions for ${selectedCaseIds.length} selected cases`}
|
||||
>
|
||||
<Icon name="check-circle" size={18} color={theme.colors.background} />
|
||||
<Text style={styles.bulkActionText}>
|
||||
Review {selectedCaseIds.length}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
{showFilters && (
|
||||
<FilterTabs
|
||||
selectedUrgencyFilter={urgencyFilter}
|
||||
selectedSeverityFilter={severityFilter}
|
||||
selectedCategoryFilter={categoryFilter}
|
||||
onUrgencyFilterChange={handleUrgencyFilterChange}
|
||||
onSeverityFilterChange={handleSeverityFilterChange}
|
||||
onCategoryFilterChange={handleCategoryFilterChange}
|
||||
onClearFilters={handleClearFilters}
|
||||
filterCounts={filterCounts}
|
||||
activeFiltersCount={activeFiltersCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Results Summary */}
|
||||
<View style={styles.resultsSummary}>
|
||||
<Text style={styles.resultsText}>
|
||||
{statistics.total} predictions found
|
||||
{activeFiltersCount > 0 && ` (${activeFiltersCount} filters applied)`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
), [
|
||||
showStats,
|
||||
statistics,
|
||||
handleStatsPress,
|
||||
searchQuery,
|
||||
handleSearch,
|
||||
showFilters,
|
||||
handleToggleFilters,
|
||||
activeFiltersCount,
|
||||
selectedCaseIds,
|
||||
handleBulkReview,
|
||||
urgencyFilter,
|
||||
severityFilter,
|
||||
categoryFilter,
|
||||
handleUrgencyFilterChange,
|
||||
handleSeverityFilterChange,
|
||||
handleCategoryFilterChange,
|
||||
handleClearFilters,
|
||||
filterCounts,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Render List Footer
|
||||
*
|
||||
* Purpose: Render pagination controls
|
||||
*/
|
||||
const renderListFooter = useCallback(() => {
|
||||
if (totalPages <= 1) return null;
|
||||
|
||||
return (
|
||||
<View style={styles.paginationContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.paginationButton, !hasPreviousPage && styles.paginationButtonDisabled]}
|
||||
onPress={handlePreviousPage}
|
||||
disabled={!hasPreviousPage}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Previous page"
|
||||
>
|
||||
<Icon name="chevron-left" size={20} color={hasPreviousPage ? theme.colors.primary : theme.colors.textMuted} />
|
||||
<Text style={[styles.paginationButtonText, !hasPreviousPage && styles.paginationButtonTextDisabled]}>
|
||||
Previous
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={styles.paginationInfo}>
|
||||
Page {currentPage} of {totalPages}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.paginationButton, !hasNextPage && styles.paginationButtonDisabled]}
|
||||
onPress={handleNextPage}
|
||||
disabled={!hasNextPage}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Next page"
|
||||
>
|
||||
<Text style={[styles.paginationButtonText, !hasNextPage && styles.paginationButtonTextDisabled]}>
|
||||
Next
|
||||
</Text>
|
||||
<Icon name="chevron-right" size={20} color={hasNextPage ? theme.colors.primary : theme.colors.textMuted} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}, [totalPages, currentPage, hasPreviousPage, hasNextPage, handlePreviousPage, handleNextPage]);
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>AI Predictions</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={() => setShowStats(!showStats)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Toggle statistics"
|
||||
>
|
||||
<Icon name={showStats ? 'eye-off' : 'eye'} size={20} color={theme.colors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
{error ? (
|
||||
<EmptyState
|
||||
title="Error Loading Predictions"
|
||||
message={error}
|
||||
iconName="alert-circle"
|
||||
actionText="Retry"
|
||||
onAction={handleRetry}
|
||||
/>
|
||||
) : isLoading && cases.length === 0 ? (
|
||||
<LoadingState message="Loading AI predictions..." />
|
||||
) : cases.length === 0 ? (
|
||||
<EmptyState
|
||||
title="No AI Predictions Found"
|
||||
message="There are no AI prediction cases matching your current filters."
|
||||
iconName="brain"
|
||||
actionText="Clear Filters"
|
||||
onAction={activeFiltersCount > 0 ? handleClearFilters : handleRefresh}
|
||||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
data={cases}
|
||||
renderItem={renderPredictionCase}
|
||||
keyExtractor={(item) => item.patid}
|
||||
ListHeaderComponent={renderListHeader}
|
||||
ListFooterComponent={renderListFooter}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
colors={[theme.colors.primary]}
|
||||
tintColor={theme.colors.primary}
|
||||
/>
|
||||
}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContent}
|
||||
accessibilityRole="list"
|
||||
/>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: theme.typography.fontSize.displaySmall,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
headerButton: {
|
||||
padding: theme.spacing.sm,
|
||||
},
|
||||
filterControls: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
},
|
||||
filterToggle: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.primary,
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
filterToggleActive: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
},
|
||||
filterToggleText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.primary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
filterToggleActiveText: {
|
||||
color: theme.colors.background,
|
||||
},
|
||||
filterBadge: {
|
||||
backgroundColor: theme.colors.error,
|
||||
borderRadius: 10,
|
||||
minWidth: 20,
|
||||
height: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
filterBadgeText: {
|
||||
fontSize: theme.typography.fontSize.caption,
|
||||
color: theme.colors.background,
|
||||
fontWeight: theme.typography.fontWeight.bold,
|
||||
},
|
||||
bulkActionButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.colors.primary,
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
bulkActionText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.background,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
resultsSummary: {
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
},
|
||||
resultsText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textSecondary,
|
||||
},
|
||||
listContent: {
|
||||
paddingBottom: theme.spacing.xl,
|
||||
},
|
||||
paginationContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.lg,
|
||||
marginTop: theme.spacing.lg,
|
||||
},
|
||||
paginationButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.primary,
|
||||
gap: theme.spacing.xs,
|
||||
},
|
||||
paginationButtonDisabled: {
|
||||
borderColor: theme.colors.textMuted,
|
||||
opacity: 0.5,
|
||||
},
|
||||
paginationButtonText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.primary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
paginationButtonTextDisabled: {
|
||||
color: theme.colors.textMuted,
|
||||
},
|
||||
paginationInfo: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textSecondary,
|
||||
fontWeight: theme.typography.fontWeight.medium,
|
||||
},
|
||||
});
|
||||
|
||||
export default AIPredictionsScreen;
|
||||
|
||||
/*
|
||||
* End of File: AIPredictionsScreen.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
14
app/modules/AIPrediction/screens/index.ts
Normal file
14
app/modules/AIPrediction/screens/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Screens exports for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export { default as AIPredictionsScreen } from './AIPredictionsScreen';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
233
app/modules/AIPrediction/services/aiPredictionAPI.ts
Normal file
233
app/modules/AIPrediction/services/aiPredictionAPI.ts
Normal file
@ -0,0 +1,233 @@
|
||||
/*
|
||||
* File: aiPredictionAPI.ts
|
||||
* Description: API service for AI prediction operations using apisauce
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import { create } from 'apisauce';
|
||||
import { API_CONFIG, buildHeaders } from '../../../shared/utils';
|
||||
|
||||
const api = create({
|
||||
baseURL: API_CONFIG.BASE_URL
|
||||
});
|
||||
|
||||
/**
|
||||
* AI Prediction API Service
|
||||
*
|
||||
* Purpose: Handle all AI prediction-related API operations
|
||||
*
|
||||
* Features:
|
||||
* - Get AI prediction results for all patients
|
||||
* - Get individual case prediction details
|
||||
* - Update case review status
|
||||
* - Search and filter predictions
|
||||
* - Get prediction statistics
|
||||
*/
|
||||
export const aiPredictionAPI = {
|
||||
/**
|
||||
* Get All AI Prediction Results
|
||||
*
|
||||
* Purpose: Fetch all AI prediction results from server
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @param params - Optional query parameters for filtering
|
||||
* @returns Promise with AI prediction cases data
|
||||
*/
|
||||
getAllPredictions: (token: string, params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
urgency?: string;
|
||||
severity?: string;
|
||||
category?: string;
|
||||
search?: string;
|
||||
}) => {
|
||||
const queryParams = params ? { ...params } : {};
|
||||
return api.get('/api/ai-cases/all-prediction-results', queryParams, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get AI Prediction Case Details
|
||||
*
|
||||
* Purpose: Fetch detailed information for a specific AI prediction case
|
||||
*
|
||||
* @param caseId - AI prediction case ID (patid)
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with detailed case prediction data
|
||||
*/
|
||||
getCaseDetails: (caseId: string, token: string) => {
|
||||
return api.get(`/api/ai-cases/prediction-details/${caseId}`, {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update Case Review Status
|
||||
*
|
||||
* Purpose: Update the review status of an AI prediction case
|
||||
*
|
||||
* @param caseId - AI prediction case ID
|
||||
* @param reviewData - Review status and notes
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with updated case data
|
||||
*/
|
||||
updateCaseReview: (caseId: string, reviewData: {
|
||||
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
|
||||
reviewed_by?: string;
|
||||
review_notes?: string;
|
||||
priority?: 'critical' | 'high' | 'medium' | 'low';
|
||||
}, token: string) => {
|
||||
return api.put(`/api/ai-cases/review/${caseId}`, reviewData, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Prediction Statistics
|
||||
*
|
||||
* Purpose: Fetch AI prediction statistics for dashboard
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @param timeRange - Optional time range filter (today, week, month)
|
||||
* @returns Promise with prediction statistics
|
||||
*/
|
||||
getPredictionStats: (token: string, timeRange?: 'today' | 'week' | 'month') => {
|
||||
const params = timeRange ? { timeRange } : {};
|
||||
return api.get('/api/ai-cases/statistics', params, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Search AI Prediction Cases
|
||||
*
|
||||
* Purpose: Search AI prediction cases by various criteria
|
||||
*
|
||||
* @param query - Search query (patient ID, hospital, findings)
|
||||
* @param token - Authentication token
|
||||
* @param filters - Additional search filters
|
||||
* @returns Promise with search results
|
||||
*/
|
||||
searchPredictions: (query: string, token: string, filters?: {
|
||||
urgency?: string[];
|
||||
severity?: string[];
|
||||
category?: string[];
|
||||
dateRange?: { start: string; end: string };
|
||||
}) => {
|
||||
const params = {
|
||||
q: query,
|
||||
...(filters && { filters: JSON.stringify(filters) })
|
||||
};
|
||||
return api.get('/api/ai-cases/search', params, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Predictions by Hospital
|
||||
*
|
||||
* Purpose: Fetch AI predictions filtered by hospital
|
||||
*
|
||||
* @param hospitalId - Hospital UUID
|
||||
* @param token - Authentication token
|
||||
* @param params - Optional query parameters
|
||||
* @returns Promise with hospital-specific predictions
|
||||
*/
|
||||
getPredictionsByHospital: (hospitalId: string, token: string, params?: {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
urgency?: string;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
}) => {
|
||||
const queryParams = params ? { ...params } : {};
|
||||
return api.get(`/api/ai-cases/hospital/${hospitalId}/predictions`, queryParams, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Critical Predictions
|
||||
*
|
||||
* Purpose: Fetch only critical and urgent AI predictions
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with critical predictions data
|
||||
*/
|
||||
getCriticalPredictions: (token: string) => {
|
||||
return api.get('/api/ai-cases/critical-predictions', {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Bulk Update Case Reviews
|
||||
*
|
||||
* Purpose: Update multiple case reviews at once
|
||||
*
|
||||
* @param caseIds - Array of case IDs to update
|
||||
* @param reviewData - Review data to apply to all cases
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with bulk update results
|
||||
*/
|
||||
bulkUpdateReviews: (caseIds: string[], reviewData: {
|
||||
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
|
||||
reviewed_by?: string;
|
||||
review_notes?: string;
|
||||
}, token: string) => {
|
||||
return api.put('/api/ai-cases/bulk-review', {
|
||||
caseIds,
|
||||
reviewData
|
||||
}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Export Predictions Data
|
||||
*
|
||||
* Purpose: Export AI predictions data for reporting
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @param filters - Export filters
|
||||
* @param format - Export format (csv, xlsx, pdf)
|
||||
* @returns Promise with export file data
|
||||
*/
|
||||
exportPredictions: (token: string, filters?: {
|
||||
urgency?: string[];
|
||||
severity?: string[];
|
||||
dateRange?: { start: string; end: string };
|
||||
hospitalId?: string;
|
||||
}, format: 'csv' | 'xlsx' | 'pdf' = 'csv') => {
|
||||
const params = {
|
||||
format,
|
||||
...(filters && { filters: JSON.stringify(filters) })
|
||||
};
|
||||
return api.get('/api/ai-cases/export', params, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Prediction Trends
|
||||
*
|
||||
* Purpose: Fetch prediction trends data for analytics
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @param period - Time period for trends (daily, weekly, monthly)
|
||||
* @returns Promise with trends data
|
||||
*/
|
||||
getPredictionTrends: (token: string, period: 'daily' | 'weekly' | 'monthly' = 'weekly') => {
|
||||
return api.get('/api/ai-cases/trends', { period }, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit Feedback on Prediction
|
||||
*
|
||||
* Purpose: Submit physician feedback on AI prediction accuracy
|
||||
*
|
||||
* @param caseId - AI prediction case ID
|
||||
* @param feedbackData - Feedback data
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with feedback submission result
|
||||
*/
|
||||
submitPredictionFeedback: (caseId: string, feedbackData: {
|
||||
accuracy_rating: 1 | 2 | 3 | 4 | 5;
|
||||
is_accurate: boolean;
|
||||
physician_diagnosis?: string;
|
||||
feedback_notes?: string;
|
||||
improvement_suggestions?: string;
|
||||
}, token: string) => {
|
||||
return api.post(`/api/ai-cases/feedback/${caseId}`, feedbackData, buildHeaders({ token }));
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* End of File: aiPredictionAPI.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
14
app/modules/AIPrediction/services/index.ts
Normal file
14
app/modules/AIPrediction/services/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Services exports for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export * from './aiPredictionAPI';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
221
app/modules/AIPrediction/types/aiPrediction.ts
Normal file
221
app/modules/AIPrediction/types/aiPrediction.ts
Normal file
@ -0,0 +1,221 @@
|
||||
/*
|
||||
* File: aiPrediction.ts
|
||||
* Description: Type definitions for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// AI PREDICTION INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AI Prediction Interface
|
||||
*
|
||||
* Purpose: Define the structure of AI prediction data from the API
|
||||
*
|
||||
* Based on API response structure:
|
||||
* - label: Type of medical finding
|
||||
* - finding_type: Category of the finding
|
||||
* - clinical_urgency: Urgency level for medical response
|
||||
* - confidence_score: AI confidence in the prediction (0-1)
|
||||
* - finding_category: General category of the finding
|
||||
* - primary_severity: Severity level of the condition
|
||||
* - anatomical_location: Where the finding is located
|
||||
*/
|
||||
export interface AIPrediction {
|
||||
label: string;
|
||||
finding_type: 'no_pathology' | 'pathology' | 'abnormal' | 'normal' | 'unknown';
|
||||
clinical_urgency: 'urgent' | 'moderate' | 'low' | 'routine' | 'emergency';
|
||||
confidence_score: number; // 0.0 to 1.0
|
||||
finding_category: 'normal' | 'abnormal' | 'critical' | 'warning' | 'unknown';
|
||||
primary_severity: 'high' | 'medium' | 'low' | 'none';
|
||||
anatomical_location: string; // 'not_applicable' | specific location
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Prediction Case Interface
|
||||
*
|
||||
* Purpose: Complete AI prediction case data structure
|
||||
*
|
||||
* Features:
|
||||
* - Patient identification
|
||||
* - Hospital association
|
||||
* - AI prediction results
|
||||
* - Metadata for tracking and display
|
||||
*/
|
||||
export interface AIPredictionCase {
|
||||
patid: string; // Patient ID
|
||||
hospital_id: string; // Hospital UUID
|
||||
prediction: AIPrediction;
|
||||
|
||||
// Additional metadata (will be added for UI purposes)
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
reviewed_by?: string;
|
||||
review_status?: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
|
||||
priority?: 'critical' | 'high' | 'medium' | 'low';
|
||||
processed_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Prediction API Response Interface
|
||||
*
|
||||
* Purpose: Define the structure of API response
|
||||
*/
|
||||
export interface AIPredictionAPIResponse {
|
||||
success: boolean;
|
||||
data: AIPredictionCase[];
|
||||
message?: string;
|
||||
total?: number;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Prediction State Interface
|
||||
*
|
||||
* Purpose: Define Redux state structure for AI predictions
|
||||
*
|
||||
* Features:
|
||||
* - Prediction cases management
|
||||
* - Current selected case
|
||||
* - Loading states for async operations
|
||||
* - Error handling and messages
|
||||
* - Search and filtering
|
||||
* - Pagination support
|
||||
* - Cache management
|
||||
*/
|
||||
export interface AIPredictionState {
|
||||
// Prediction data
|
||||
predictionCases: AIPredictionCase[];
|
||||
currentCase: AIPredictionCase | null;
|
||||
|
||||
// Loading states
|
||||
isLoading: boolean;
|
||||
isRefreshing: boolean;
|
||||
isLoadingCaseDetails: boolean;
|
||||
|
||||
// Error handling
|
||||
error: string | null;
|
||||
|
||||
// Search and filtering
|
||||
searchQuery: string;
|
||||
selectedUrgencyFilter: 'all' | 'urgent' | 'moderate' | 'low' | 'routine' | 'emergency';
|
||||
selectedSeverityFilter: 'all' | 'high' | 'medium' | 'low' | 'none';
|
||||
selectedCategoryFilter: 'all' | 'normal' | 'abnormal' | 'critical' | 'warning' | 'unknown';
|
||||
sortBy: 'date' | 'urgency' | 'confidence' | 'severity';
|
||||
sortOrder: 'asc' | 'desc';
|
||||
|
||||
// Pagination
|
||||
currentPage: number;
|
||||
itemsPerPage: number;
|
||||
totalItems: number;
|
||||
|
||||
// Cache management
|
||||
lastUpdated: String | null;
|
||||
cacheExpiry: String | null;
|
||||
|
||||
// UI state
|
||||
showFilters: boolean;
|
||||
selectedCaseIds: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Prediction Filter Options
|
||||
*
|
||||
* Purpose: Define available filter options for the UI
|
||||
*/
|
||||
export interface AIPredictionFilters {
|
||||
urgency: Array<{
|
||||
label: string;
|
||||
value: AIPredictionState['selectedUrgencyFilter'];
|
||||
count?: number;
|
||||
}>;
|
||||
severity: Array<{
|
||||
label: string;
|
||||
value: AIPredictionState['selectedSeverityFilter'];
|
||||
count?: number;
|
||||
}>;
|
||||
category: Array<{
|
||||
label: string;
|
||||
value: AIPredictionState['selectedCategoryFilter'];
|
||||
count?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Prediction Statistics Interface
|
||||
*
|
||||
* Purpose: Define statistics data for dashboard display
|
||||
*/
|
||||
export interface AIPredictionStats {
|
||||
totalCases: number;
|
||||
criticalCases: number;
|
||||
urgentCases: number;
|
||||
reviewedCases: number;
|
||||
pendingCases: number;
|
||||
averageConfidence: number;
|
||||
todaysCases: number;
|
||||
weeklyTrend: number; // percentage change from last week
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Prediction Navigation Props
|
||||
*
|
||||
* Purpose: Type safety for navigation between AI prediction screens
|
||||
*/
|
||||
export type AIPredictionNavigationProps = {
|
||||
AIPredictionList: undefined;
|
||||
AIPredictionDetails: { caseId: string };
|
||||
AIPredictionFilters: undefined;
|
||||
AIPredictionStats: undefined;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// UTILITY TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Prediction Urgency Colors
|
||||
*
|
||||
* Purpose: Map urgency levels to UI colors
|
||||
*/
|
||||
export const URGENCY_COLORS = {
|
||||
emergency: '#F44336', // Red
|
||||
urgent: '#FF5722', // Deep Orange
|
||||
moderate: '#FF9800', // Orange
|
||||
low: '#FFC107', // Amber
|
||||
routine: '#4CAF50', // Green
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Prediction Severity Colors
|
||||
*
|
||||
* Purpose: Map severity levels to UI colors
|
||||
*/
|
||||
export const SEVERITY_COLORS = {
|
||||
high: '#F44336', // Red
|
||||
medium: '#FF9800', // Orange
|
||||
low: '#FFC107', // Amber
|
||||
none: '#4CAF50', // Green
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Finding Category Colors
|
||||
*
|
||||
* Purpose: Map finding categories to UI colors
|
||||
*/
|
||||
export const CATEGORY_COLORS = {
|
||||
critical: '#F44336', // Red
|
||||
abnormal: '#FF9800', // Orange
|
||||
warning: '#FFC107', // Amber
|
||||
normal: '#4CAF50', // Green
|
||||
unknown: '#9E9E9E', // Gray
|
||||
} as const;
|
||||
|
||||
/*
|
||||
* End of File: aiPrediction.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
14
app/modules/AIPrediction/types/index.ts
Normal file
14
app/modules/AIPrediction/types/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* File: index.ts
|
||||
* Description: Type definitions exports for AI Prediction module
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
export * from './aiPrediction';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -10,6 +10,7 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { theme } from '../theme/theme';
|
||||
import { DashboardStackNavigator } from '../modules/Dashboard/navigation';
|
||||
import { SettingsStackNavigator } from '../modules/Settings/navigation';
|
||||
import { AIPredictionStackNavigator } from '../modules/AIPrediction/navigation';
|
||||
import { MainTabParamList } from './navigationTypes';
|
||||
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
|
||||
import { ComingSoonScreen } from '../shared/components';
|
||||
@ -26,7 +27,7 @@ const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||
* Tab Structure:
|
||||
* - Dashboard: Main ER dashboard with patient overview and statistics
|
||||
* - Patients: Detailed patient list and management interface
|
||||
* - Alerts: Critical notifications and alert management
|
||||
* - AI Predictions: AI-powered medical case predictions and analysis
|
||||
* - Reports: Medical reports and documentation access
|
||||
* - Settings: User preferences and app configuration
|
||||
*
|
||||
@ -96,16 +97,15 @@ export const MainTabNavigator: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Alerts Tab - Critical notifications */}
|
||||
{/* AI Predictions Tab - AI-powered medical predictions */}
|
||||
<Tab.Screen
|
||||
name="Alerts"
|
||||
component={ComingSoonScreen} // TODO: Replace with actual AlertsScreen
|
||||
name="AIPredictions"
|
||||
component={AIPredictionStackNavigator}
|
||||
options={{
|
||||
title: 'Alerts',
|
||||
tabBarLabel: 'Alerts',
|
||||
// TODO: Add tab bar icon
|
||||
title: 'AI Predictions',
|
||||
tabBarLabel: 'AI Predictions',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<MaterialIcons name="notifications" size={size} color={color} />
|
||||
<MaterialIcons name="psychology" size={size} color={color} />
|
||||
),
|
||||
headerShown: false,
|
||||
}}
|
||||
|
||||
@ -39,16 +39,14 @@ export type RootStackParamList = {
|
||||
* Tabs:
|
||||
* - Dashboard: ER dashboard with patient overview
|
||||
* - Patients: Patient list and management
|
||||
* - Alerts: Critical notifications and alerts
|
||||
* - Reports: Medical reports and documentation
|
||||
* - AIPredictions: AI-powered medical case predictions and analysis
|
||||
* - Settings: User preferences and app configuration
|
||||
*/
|
||||
export type MainTabParamList = {
|
||||
Dashboard: DashboardScreenParams; // Dashboard with initial data
|
||||
Patients: PatientsScreenParams; // Patient list screen
|
||||
Alerts: AlertsScreenParams; // Alerts screen
|
||||
Reports: ReportsScreenParams; // Reports screen
|
||||
Settings: SettingsScreenParams; // Settings screen
|
||||
Dashboard: DashboardScreenParams; // Dashboard with initial data
|
||||
Patients: PatientsScreenParams; // Patient list screen
|
||||
AIPredictions: AIPredictionScreenParams; // AI predictions screen
|
||||
Settings: SettingsScreenParams; // Settings screen
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
@ -86,17 +84,19 @@ export interface PatientsScreenParams {
|
||||
}
|
||||
|
||||
/**
|
||||
* AlertsScreenParams
|
||||
* AIPredictionScreenParams
|
||||
*
|
||||
* Purpose: Parameters for the alerts screen
|
||||
* Purpose: Parameters for the AI predictions screen
|
||||
*
|
||||
* Parameters:
|
||||
* - priority: Optional priority filter for alerts
|
||||
* - unreadOnly: Optional flag to show only unread alerts
|
||||
* - filter: Optional filter for prediction types
|
||||
* - urgency: Optional urgency level filter
|
||||
* - searchQuery: Optional search term for predictions
|
||||
*/
|
||||
export interface AlertsScreenParams {
|
||||
priority?: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
|
||||
unreadOnly?: boolean;
|
||||
export interface AIPredictionScreenParams {
|
||||
filter?: 'all' | 'critical' | 'urgent' | 'pending' | 'reviewed';
|
||||
urgency?: 'emergency' | 'urgent' | 'moderate' | 'low' | 'routine';
|
||||
searchQuery?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -12,6 +12,24 @@ export * from './alerts';
|
||||
export * from './settings';
|
||||
export * from './common';
|
||||
|
||||
// AI Prediction types (re-export from module)
|
||||
export type {
|
||||
AIPrediction,
|
||||
AIPredictionCase,
|
||||
AIPredictionAPIResponse,
|
||||
AIPredictionState,
|
||||
AIPredictionStats,
|
||||
AIPredictionFilters,
|
||||
AIPredictionNavigationProps,
|
||||
} from '../modules/AIPrediction/types';
|
||||
|
||||
// AI Prediction constants (re-export from module)
|
||||
export {
|
||||
URGENCY_COLORS,
|
||||
SEVERITY_COLORS,
|
||||
CATEGORY_COLORS,
|
||||
} from '../modules/AIPrediction/types';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
|
||||
@ -17,6 +17,7 @@ import alertsReducer from '../modules/Dashboard/redux/alertsSlice';
|
||||
import settingsReducer from '../modules/Settings/redux/settingsSlice';
|
||||
import uiReducer from '../modules/Dashboard/redux/uiSlice';
|
||||
import hospitalReducer from '../modules/Auth/redux/hospitalSlice';
|
||||
import aiPredictionReducer from '../modules/AIPrediction/redux/aiPredictionSlice';
|
||||
|
||||
// ============================================================================
|
||||
// REDUX PERSIST CONFIGURATION
|
||||
@ -48,6 +49,7 @@ const persistConfig = {
|
||||
'auth', // Authentication state (user login, tokens)
|
||||
'settings', // User preferences and settings
|
||||
'patientCare', // Patient data cache
|
||||
'aiPrediction', // AI prediction data cache
|
||||
],
|
||||
|
||||
// Blacklist: Don't persist these reducers
|
||||
@ -84,6 +86,7 @@ const persistConfig = {
|
||||
* - auth: Authentication and user management
|
||||
* - dashboard: ER dashboard data and statistics
|
||||
* - patientCare: Patient information and medical records
|
||||
* - aiPrediction: AI prediction cases and analysis
|
||||
* - alerts: Critical alerts and notifications
|
||||
* - settings: User preferences and app settings
|
||||
* - ui: User interface state (loading, modals, etc.)
|
||||
@ -92,6 +95,7 @@ const rootReducer = combineReducers({
|
||||
auth: authReducer,
|
||||
dashboard: dashboardReducer,
|
||||
patientCare: patientCareReducer,
|
||||
aiPrediction: aiPredictionReducer,
|
||||
alerts: alertsReducer,
|
||||
settings: settingsReducer,
|
||||
ui: uiReducer,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user