Compare commits

...

10 Commits

Author SHA1 Message Date
yashwin-foxy
8fe78a6d7c code swapped with physician code 2025-08-20 10:41:26 +05:30
yashwin-foxy
70d4ec6690 after adding series details page give for client testing 2025-08-19 19:45:28 +05:30
yashwin-foxy
266de3b512 dashboard modifiedd 2025-08-18 20:30:00 +05:30
yashwin-foxy
467dc0b8cf dashboard added and profile update 2025-08-14 20:16:03 +05:30
yashwin-foxy
692a8156da ai prediction detil screen added 2025-08-13 18:21:57 +05:30
yashwin-foxy
413a1d74de added patient detail screen 2025-08-12 18:50:19 +05:30
yashwin-foxy
80a1688e19 crearted UI and integrated Api for Physician suggestion 2025-08-11 19:23:19 +05:30
yashwin-foxy
e8492cd442 ai prediction data mapped to the new tab called ai prediction 2025-08-08 19:03:14 +05:30
yashwin-foxy
84b63e401f patient data fetched to patient tab 2025-08-07 19:42:41 +05:30
yashwin-foxy
9eb1416866 upload document type modified 2025-08-07 17:22:16 +05:30
156 changed files with 23283 additions and 658 deletions

View File

@ -7,7 +7,7 @@ alwaysApply: true
### 1. Root Level Organization
```
NeoScan_Physician/
NeoScan_Radiologist/
├── app/ # Main application code
├── docs/ # Documentation
├── android/ # Android native code

View File

@ -10,7 +10,7 @@
## 📁 Complete Directory Structure
```
NeoScan_Physician/
NeoScan_Radiologist/
├── app/ # Main application code
│ ├── modules/ # Feature-based modules
│ │ ├── Auth/ # Authentication module
@ -29,7 +29,7 @@ NeoScan_Physician/
│ │ │ │ ├── QuickActions.tsx # Emergency quick actions
│ │ │ │ └── DepartmentStats.tsx # Department statistics
│ │ │ ├── screens/ # Dashboard screens
│ │ │ │ └── ERDashboardScreen.tsx # Main ER dashboard
│ │ │ │ └── DashboardScreen.tsx # Main ER dashboard
│ │ │ ├── hooks/ # Dashboard custom hooks
│ │ │ ├── redux/ # Dashboard state management
│ │ │ ├── services/ # Dashboard API services
@ -150,7 +150,7 @@ NeoScan_Physician/
│ │ ├── AndroidManifest.xml # Main manifest
│ │ ├── java/ # Java source
│ │ │ └── com/ # Package structure
│ │ │ └── neoscan_physician/
│ │ │ └── neoscan_radiologist/
│ │ │ ├── MainActivity.kt # Main activity
│ │ │ └── MainApplication.kt # Application class
│ │ └── res/ # Resources
@ -166,7 +166,7 @@ NeoScan_Physician/
│ ├── gradlew.bat # Windows gradle wrapper
│ └── settings.gradle # Gradle settings
├── ios/ # iOS native code
│ ├── NeoScan_Physician/ # iOS app
│ ├── NeoScan_Radiologist/ # iOS app
│ │ ├── AppDelegate.swift # App delegate
│ │ ├── Images.xcassets/ # Image assets
│ │ │ ├── AppIcon.appiconset/ # App icons
@ -174,11 +174,11 @@ NeoScan_Physician/
│ │ ├── Info.plist # App info
│ │ ├── LaunchScreen.storyboard # Launch screen
│ │ └── PrivacyInfo.xcprivacy # Privacy info
│ ├── NeoScan_Physician.xcodeproj/ # Xcode project
│ ├── NeoScan_Radiologist.xcodeproj/ # Xcode project
│ │ ├── project.pbxproj # Project file
│ │ └── xcshareddata/ # Shared data
│ │ └── xcschemes/ # Build schemes
│ │ └── NeoScan_Physician.xcscheme
│ │ └── NeoScan_Radiologist.xcscheme
│ └── Podfile # CocoaPods configuration
├── __tests__/ # Test files
│ ├── App.test.tsx # App component tests
@ -223,7 +223,7 @@ NeoScan_Physician/
### Dashboard Module
**Purpose**: Main ER dashboard with patient monitoring and alerts
- **ERDashboardScreen**: Main dashboard with patient list and statistics
- **DashboardScreen**: Main dashboard with patient list and statistics
- **PatientCard**: Individual patient information display
- **CriticalAlerts**: High-priority alert notifications
- **QuickActions**: Emergency procedure shortcuts

View File

@ -42,7 +42,7 @@ A comprehensive React Native application designed for emergency department physi
### Project Structure
```
NeoScan_Physician/
NeoScan_Radiologist/
├── app/ # Main application code
│ ├── modules/ # Feature-based modules
│ │ ├── Auth/ # Authentication module
@ -120,7 +120,7 @@ NeoScan_Physician/
1. **Clone the repository**
```bash
git clone <repository-url>
cd NeoScan_Physician
cd NeoScan_Radiologist
```
2. **Install dependencies**

View File

@ -71,6 +71,7 @@ def enableProguardInReleaseBuilds = false
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def enableSeparateBuildPerCPUArchitecture = true
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
android {
@ -78,9 +79,16 @@ android {
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace "com.neoscan_physician"
namespace "com.neoscan_radiologist"
splits {
abi {
enable true
include 'armeabi-v7a', 'arm64-v8a', 'x86'
universalApk false
}
}
defaultConfig {
applicationId "com.neoscan_physician"
applicationId "com.neoscan_radiologist"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,4 +1,4 @@
package com.neoscan_physician
package com.neoscan_radiologist
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
@ -11,7 +11,7 @@ class MainActivity : ReactActivity() {
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "NeoScan_Physician"
override fun getMainComponentName(): String = "NeoScan_Radiologist"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]

View File

@ -1,4 +1,4 @@
package com.neoscan_physician
package com.neoscan_radiologist
import android.app.Application
import com.facebook.react.PackageList

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -1,3 +1,3 @@
<resources>
<string name="app_name">NeoScanPhysician</string>
<string name="app_name">Radiologist</string>
</resources>

View File

@ -32,7 +32,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=true
newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.

View File

@ -2,36 +2,36 @@
"migIndex": 1,
"data": [
{
"path": "app/assets/fonts/Roboto-Black.ttf",
"sha1": "d1678489a8d5645f16486ec52d77b651ff0bf327"
"path": "app/assets/fonts/WorkSans-Bold.ttf",
"sha1": "ec84061651ead3c3c5cbb61c2d338aca0bacdc1e"
},
{
"path": "app/assets/fonts/Roboto-Bold.ttf",
"sha1": "508c35dee818addce6cc6d1fb6e42f039da5a7cf"
"path": "app/assets/fonts/WorkSans-ExtraBold.ttf",
"sha1": "0b371d1dbfbdd15db880bbd129b239530c71accb"
},
{
"path": "app/assets/fonts/Roboto-ExtraBold.ttf",
"sha1": "3dbfd71b6fbcfbd8e7ee8a8dd033dc5aaad63249"
"path": "app/assets/fonts/WorkSans-ExtraLight.ttf",
"sha1": "74596e55487e2961b6c43993698d658e2ceee77b"
},
{
"path": "app/assets/fonts/Roboto-ExtraLight.ttf",
"sha1": "df556e64732e5c272349e13cb5f87591a1ae779b"
"path": "app/assets/fonts/WorkSans-Light.ttf",
"sha1": "293e11dae7e8b930bf5eea0b06ca979531f22189"
},
{
"path": "app/assets/fonts/Roboto-Light.ttf",
"sha1": "318b44c0a32848f78bf11d4fbf3355d00647a796"
"path": "app/assets/fonts/WorkSans-Medium.ttf",
"sha1": "c281f8454dd193c2260e43ae2de171c5dd4086e4"
},
{
"path": "app/assets/fonts/Roboto-Medium.ttf",
"sha1": "fa5192203f85ddb667579e1bdf26f12098bb873b"
"path": "app/assets/fonts/WorkSans-Regular.ttf",
"sha1": "5e0183b29b57c54595c62ac6bc223b21f1434226"
},
{
"path": "app/assets/fonts/Roboto-Regular.ttf",
"sha1": "3bff51436aa7eb995d84cfc592cc63e1316bb400"
"path": "app/assets/fonts/WorkSans-SemiBold.ttf",
"sha1": "64b8fe156fafce221a0f66504255257053fc6062"
},
{
"path": "app/assets/fonts/Roboto-SemiBold.ttf",
"sha1": "9ca139684fe902c8310dd82991648376ac9838db"
"path": "app/assets/fonts/WorkSans-Thin.ttf",
"sha1": "a62251331038fdd079c47bc413a350efbf702db8"
}
]
}

View File

@ -1,6 +1,6 @@
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
rootProject.name = 'NeoScan_Physician'
rootProject.name = 'NeoScan_Radiologist'
include ':app'
includeBuild('../node_modules/@react-native/gradle-plugin')

View File

@ -1,4 +1,4 @@
{
"name": "NeoScan_Physician",
"displayName": "NeoScan_Physician"
"name": "NeoScan_Radiologist",
"displayName": "NeoScan_Radiologist"
}

View File

@ -0,0 +1,726 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>DICOM Viewer - Mobile Friendly</title>
<style>
#dicomImage {
width: 100%;
max-width: 512px;
height: 400px;
margin: 10px auto;
border: 1px solid #333;
background: black;
touch-action: none;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.preview-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 20px;
text-align: center;
min-height: 300px;
}
.preview-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.7;
display: block;
}
.preview-text {
font-size: 16px;
color: #aaa;
margin-bottom: 8px;
display: block;
width: 100%;
}
.preview-subtext {
font-size: 14px;
color: #666;
line-height: 1.4;
display: block;
width: 100%;
}
.status {
background: #1a1a1a;
padding: 10px;
margin: 10px 0;
border-radius: 8px;
font-size: 14px;
width: 100%;
box-sizing: border-box;
}
.loading {
display: none;
color: #2196F3;
font-weight: bold;
}
.error {
color: #f44336;
background: #2d1b1b;
padding: 10px;
border-radius: 6px;
margin: 10px 0;
width: 100%;
box-sizing: border-box;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
align-items: center;
margin: 10px 0;
width: 100%;
box-sizing: border-box;
}
.control-group {
display: flex;
gap: 5px;
align-items: center;
flex-wrap: wrap;
justify-content: center;
}
.btn {
background: #2196F3;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
touch-action: manipulation;
min-height: 44px;
min-width: 44px;
white-space: nowrap;
}
.btn:active {
background: #1976D2;
transform: scale(0.95);
}
.btn.secondary {
background: #666;
}
.btn.danger {
background: #f44336;
}
.btn.success {
background: #4caf50;
}
.btn:disabled {
background: #555;
opacity: 0.6;
cursor: not-allowed;
}
.zoom-controls {
display: flex;
gap: 5px;
align-items: center;
}
.zoom-btn {
width: 44px;
height: 44px;
border-radius: 22px;
font-size: 18px;
font-weight: bold;
}
.frame-controls {
display: flex;
gap: 5px;
align-items: center;
}
.frame-nav {
display: flex;
gap: 5px;
align-items: center;
}
.frame-counter {
background: #333;
padding: 8px 12px;
border-radius: 6px;
min-width: 80px;
font-size: 14px;
text-align: center;
}
.header {
position: sticky;
top: 0;
background: #1a1a1a;
padding: 10px;
z-index: 100;
border-bottom: 1px solid #333;
width: 100%;
box-sizing: border-box;
}
.header h2 {
margin: 0 0 10px 0;
font-size: 20px;
}
body {
background: #111;
color: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
text-align: center;
margin: 0;
padding: 0;
overflow-x: hidden;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
* {
box-sizing: border-box;
}
@media (max-width: 480px) {
.controls {
flex-direction: column;
gap: 10px;
padding: 0 10px;
}
.control-group {
justify-content: center;
width: 100%;
}
.btn {
min-height: 48px;
min-width: 48px;
font-size: 16px;
flex: 1;
max-width: 120px;
}
.zoom-btn {
width: 48px;
height: 48px;
flex: none;
}
.preview-icon {
font-size: 48px;
}
.preview-text {
font-size: 14px;
}
.preview-subtext {
font-size: 12px;
}
.header {
padding: 15px 10px;
}
.header h2 {
font-size: 18px;
}
#dicomImage {
height: 300px;
margin: 10px;
max-width: calc(100% - 20px);
}
.preview-container {
min-height: 250px;
padding: 15px;
}
.status {
margin: 10px;
padding: 15px;
}
.frame-counter {
min-width: 60px;
font-size: 11px;
}
}
@media (max-width: 360px) {
.controls {
gap: 8px;
}
.btn {
min-height: 44px;
min-width: 44px;
font-size: 14px;
padding: 6px 8px;
}
.zoom-btn {
width: 44px;
height: 44px;
}
#dicomImage {
height: 250px;
}
.preview-container {
min-height: 200px;
padding: 10px;
}
.preview-icon {
font-size: 40px;
}
.preview-text {
font-size: 13px;
}
.preview-subtext {
font-size: 11px;
}
}
/* Touch feedback for mobile */
@media (hover: none) and (pointer: coarse) {
.btn:active {
transform: scale(0.95);
background: #1976D2;
}
}
/* Loading animation */
.loading {
display: inline-block;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
</head>
<body>
<div class="header">
<h2>DICOM Viewer</h2>
<div class="status">
<span id="statusText">Ready to load DICOM files</span>
<span class="loading" id="loadingText">Loading...</span>
</div>
</div>
<div class="controls">
<div class="control-group">
<button class="btn secondary" onclick="resetView()">Reset View</button>
<button class="btn danger" onclick="clearView()">Clear</button>
</div>
<div class="control-group zoom-controls">
<button class="btn zoom-btn" onclick="zoomIn()">+</button>
<button class="btn zoom-btn" onclick="zoomOut()"></button>
<button class="btn secondary" onclick="fitToWindow()">Fit</button>
</div>
<div class="control-group frame-controls">
<div class="frame-nav">
<button class="btn secondary" onclick="previousFrame()" id="prevFrameBtn" disabled></button>
<div class="frame-counter" id="frameInfo">No images</div>
<button class="btn secondary" onclick="nextFrame()" id="nextFrameBtn" disabled></button>
</div>
</div>
</div>
<div id="dicomImage">
<div class="preview-container">
<div class="preview-icon">🩻</div>
<div class="preview-text">DICOM Viewer</div>
<div class="preview-subtext">No image loaded<br>DICOM files will be loaded from parent component</div>
</div>
</div>
<!-- Libraries -->
<script src="https://cdn.jsdelivr.net/npm/cornerstone-core@2.6.1/dist/cornerstone.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dicom-parser/dist/dicomParser.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cornerstone-wado-image-loader@4.13.2/dist/cornerstoneWADOImageLoader.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/cornerstone-tools@4.22.1/dist/cornerstoneTools.min.js"></script>
<script>
const element = document.getElementById('dicomImage');
const frameInfo = document.getElementById('frameInfo');
const statusText = document.getElementById('statusText');
const loadingText = document.getElementById('loadingText');
const prevFrameBtn = document.getElementById('prevFrameBtn');
const nextFrameBtn = document.getElementById('nextFrameBtn');
let currentStack = null;
let currentImage = null;
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
cornerstoneWADOImageLoader.external.dicomParser = dicomParser;
cornerstone.enable(element);
cornerstoneTools.init({ showSVGCursors: false });
// Add mobile-friendly tools
cornerstoneTools.addTool(cornerstoneTools.PanMultiTouchTool);
cornerstoneTools.addTool(cornerstoneTools.ZoomTouchPinchTool);
cornerstoneTools.addTool(cornerstoneTools.StackScrollMultiTouchTool);
cornerstoneTools.addTool(cornerstoneTools.WwwcRegionTool);
// Activate touch tools
cornerstoneTools.setToolActive('PanMultiTouch', {});
cornerstoneTools.setToolActive('ZoomTouchPinch', {});
cornerstoneTools.setToolActive('StackScrollMultiTouch', {});
cornerstoneTools.setToolActive('WwwcRegion', {});
// Function to load DICOM from parent component
function loadDicomFromParent(imageId) {
showLoading();
loadDicom(imageId);
}
// Function to load series from parent component
function loadSeriesFromParent(imageIds) {
showLoading();
loadSeries(imageIds);
}
// Listen for messages from parent component (React Native)
window.addEventListener('message', function(event) {
try {
const message = event.data;
console.log('Received message from parent:', message);
console.log('Message type:', typeof message);
console.log('Message length:', message ? message.length : 'undefined');
// If message is a string URL, treat it as a DICOM URL
if (typeof message === 'string' && message.trim()) {
const url = message.trim();
console.log('Processing string message as URL:', url);
if (url.startsWith('http') || url.startsWith('wadouri:')) {
console.log('Loading DICOM from parent URL:', url);
showLoading();
loadDicom(url);
} else {
console.log('Invalid URL format:', url);
showError('Invalid URL format received from parent: ' + url);
}
}
// If message is an object with URL property
else if (typeof message === 'object' && message.url) {
console.log('Loading DICOM from parent object:', message.url);
showLoading();
loadDicom(message.url);
}
// If message is an object with imageIds property (for series)
else if (typeof message === 'object' && message.imageIds && Array.isArray(message.imageIds)) {
console.log('Loading series from parent:', message.imageIds);
showLoading();
loadSeries(message.imageIds);
}
// If message has a type and data
else if (typeof message === 'object' && message.type === 'loadDicom' && message.data) {
console.log('Loading DICOM from parent with type:', message.data);
showLoading();
if (Array.isArray(message.data)) {
loadSeries(message.data);
} else {
loadDicom(message.data);
}
}
else {
console.log('Message format not recognized:', message);
console.log('Message keys:', message && typeof message === 'object' ? Object.keys(message) : 'N/A');
}
} catch (error) {
console.error('Error processing message from parent:', error);
showError('Error processing message from parent: ' + error.message);
}
});
// Also listen for postMessage calls directly
window.addEventListener('load', function() {
console.log('DICOM Viewer loaded and ready to receive messages');
console.log('Window ReactNativeWebView available:', !!window.ReactNativeWebView);
// Notify parent that viewer is ready
if (window.ReactNativeWebView) {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'ready',
message: 'DICOM Viewer is ready to receive URLs'
}));
console.log('Sent ready message to parent');
}
// Also try to notify via postMessage
try {
window.parent.postMessage({
type: 'ready',
message: 'DICOM Viewer is ready to receive URLs'
}, '*');
console.log('Sent ready message via postMessage');
} catch (e) {
console.log('Could not send postMessage:', e);
}
});
// Debug function to test loading
function testLoadDicom() {
console.log('Testing DICOM loading...');
const testUrl = 'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm';
console.log('Test URL:', testUrl);
showLoading();
loadDicom(testUrl);
}
// Add test button to header for debugging
document.addEventListener('DOMContentLoaded', function() {
const header = document.querySelector('.header');
if (header) {
const testButton = document.createElement('button');
testButton.textContent = 'Test Load';
testButton.className = 'btn success';
testButton.style.marginLeft = '10px';
testButton.onclick = testLoadDicom;
header.appendChild(testButton);
console.log('Added test button');
}
});
function showLoading() {
loadingText.style.display = 'inline';
statusText.style.display = 'none';
}
function hideLoading() {
loadingText.style.display = 'none';
statusText.style.display = 'inline';
}
function showError(message) {
hideLoading();
statusText.textContent = message;
statusText.style.color = '#f44336';
}
function showSuccess(message) {
hideLoading();
statusText.textContent = message;
statusText.style.color = '#4caf50';
}
function loadDicom(imageId) {
console.log('loadDicom called with:', imageId);
// Clear the preview container
element.innerHTML = '';
// Show loading state
showLoading();
cornerstone.loadImage(imageId).then(image => {
console.log('DICOM image loaded successfully:', image);
currentImage = image;
// Display the image
cornerstone.displayImage(element, image);
console.log('Image displayed on element');
const numFrames = parseInt(image.data.string('x00280008') || '1', 10);
console.log('Number of frames:', numFrames);
if (numFrames > 1) {
const stack = { currentImageIdIndex: 0, imageIds: [] };
for (let i = 0; i < numFrames; i++) {
stack.imageIds.push(imageId + `&frame=${i}`);
}
console.log('Setting up stack with frames:', stack.imageIds.length);
setupStack(stack);
} else {
currentStack = null;
updateFrameInfo();
}
showSuccess('DICOM loaded successfully');
fitToWindow();
console.log('DICOM loading completed');
}).catch(err => {
console.error('Error loading DICOM:', err);
console.error('Error details:', {
message: err.message,
stack: err.stack,
name: err.name
});
// Show error in the viewer
element.innerHTML = `
<div class="preview-container">
<div class="preview-icon"></div>
<div class="preview-text">Error Loading DICOM</div>
<div class="preview-subtext">${err.message}<br>URL: ${imageId}</div>
</div>
`;
showError('Error loading DICOM: ' + err.message);
});
}
function loadSeries(imageIds) {
const stack = { currentImageIdIndex: 0, imageIds };
cornerstone.loadImage(imageIds[0]).then(image => {
currentImage = image;
cornerstone.displayImage(element, image);
setupStack(stack);
showSuccess(`Series loaded: ${imageIds.length} images`);
fitToWindow();
}).catch(err => {
showError('Error loading series: ' + err.message);
});
}
function setupStack(stack) {
currentStack = stack;
cornerstoneTools.addStackStateManager(element, ['stack']);
cornerstoneTools.addToolState(element, 'stack', stack);
updateFrameInfo();
element.addEventListener('cornerstonetoolsstackscroll', () => {
updateFrameInfo();
});
}
function updateFrameInfo() {
if (currentStack) {
frameInfo.textContent = `${currentStack.currentImageIdIndex + 1} / ${currentStack.imageIds.length}`;
prevFrameBtn.disabled = currentStack.currentImageIdIndex === 0;
nextFrameBtn.disabled = currentStack.currentImageIdIndex === currentStack.imageIds.length - 1;
} else {
frameInfo.textContent = 'No images';
prevFrameBtn.disabled = true;
nextFrameBtn.disabled = true;
}
}
function previousFrame() {
if (currentStack && currentStack.currentImageIdIndex > 0) {
currentStack.currentImageIdIndex--;
cornerstone.loadImage(currentStack.imageIds[currentStack.currentImageIdIndex]).then(image => {
cornerstone.displayImage(element, image);
updateFrameInfo();
});
}
}
function nextFrame() {
if (currentStack && currentStack.currentImageIdIndex < currentStack.imageIds.length - 1) {
currentStack.currentImageIdIndex++;
cornerstone.loadImage(currentStack.imageIds[currentStack.currentImageIdIndex]).then(image => {
cornerstone.displayImage(element, image);
updateFrameInfo();
});
}
}
function zoomIn() {
const viewport = cornerstone.getViewport(element);
viewport.scale *= 1.5;
cornerstone.setViewport(element, viewport);
}
function zoomOut() {
const viewport = cornerstone.getViewport(element);
viewport.scale /= 1.5;
cornerstone.setViewport(element, viewport);
}
function fitToWindow() {
if (currentImage) {
cornerstone.fitToWindow(element);
}
}
function resetView() {
if (currentImage) {
cornerstone.reset(element);
fitToWindow();
}
}
function clearView() {
cornerstone.disable(element);
cornerstone.enable(element);
currentStack = null;
currentImage = null;
updateFrameInfo();
statusText.textContent = 'Ready to load DICOM files';
statusText.style.color = 'white';
// Show preview again
element.innerHTML = `
<div class="preview-container">
<div class="preview-icon">🩻</div>
<div class="preview-text">DICOM Viewer</div>
<div class="preview-subtext">No image loaded<br>DICOM files will be loaded from parent component</div>
</div>
`;
// Reactivate tools
cornerstoneTools.setToolActive('PanMultiTouch', {});
cornerstoneTools.setToolActive('ZoomTouchPinch', {});
cornerstoneTools.setToolActive('StackScrollMultiTouch', {});
cornerstoneTools.setToolActive('WwwcRegion', {});
}
// Initialize
updateFrameInfo();
// Expose functions for parent component
window.DicomViewer = {
loadDicom: loadDicomFromParent,
loadSeries: loadSeriesFromParent,
resetView: resetView,
clearView: clearView,
zoomIn: zoomIn,
zoomOut: zoomOut,
fitToWindow: fitToWindow,
previousFrame: previousFrame,
nextFrame: nextFrame
};
</script>
</body>
</html>

View File

@ -0,0 +1,443 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DICOM Viewer Test</title>
<style>
body {
margin: 0;
padding: 20px;
background: #f5f5f5;
font-family: Arial, sans-serif;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.test-section {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.url-input {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
margin-bottom: 10px;
}
.load-button {
background: #2196F3;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.load-button:hover {
background: #1976D2;
}
.viewer-container {
margin-top: 20px;
border: 2px solid #ddd;
border-radius: 8px;
overflow: hidden;
min-height: 400px;
}
.status {
padding: 10px;
background: #f0f0f0;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
#dicomImage {
width: 100%;
height: 400px;
background: #000;
display: flex;
justify-content: center;
align-items: center;
color: white;
}
.error {
color: #F44336;
background: #FFEBEE;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.success {
color: #4CAF50;
background: #E8F5E8;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.warning {
color: #FF9800;
background: #FFF3E0;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
}
.sample-urls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.sample-url {
background: #E3F2FD;
padding: 10px;
border-radius: 4px;
cursor: pointer;
border: 1px solid #BBDEFB;
}
.sample-url:hover {
background: #BBDEFB;
}
.dicom-info {
background: #F5F5F5;
padding: 15px;
border-radius: 4px;
margin: 15px 0;
font-family: monospace;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>DICOM Viewer Test</h1>
<p>Test the DICOM viewer functionality in your browser before using it in React Native.</p>
<div class="test-section">
<h3>Sample DICOM URLs</h3>
<div class="sample-urls">
<div class="sample-url" onclick="loadSampleUrl('https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm')">
<strong>Sample 1:</strong><br>
LIDC-IDRI-0001
</div>
<div class="sample-url" onclick="loadSampleUrl('https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-002.dcm')">
<strong>Sample 2:</strong><br>
LIDC-IDRI-0001
</div>
<div class="sample-url" onclick="loadSampleUrl('https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-003.dcm')">
<strong>Sample 3:</strong><br>
LIDC-IDRI-0001
</div>
</div>
</div>
<div class="test-section">
<h3>Custom DICOM URL</h3>
<input type="text" id="customUrl" class="url-input" placeholder="Enter DICOM URL here..." />
<button onclick="loadCustomUrl()" class="load-button">Load DICOM Image</button>
</div>
<div class="viewer-container">
<div class="status" id="status">Ready to load DICOM image</div>
<div id="dicomImage">
<div>Click a sample URL above or enter a custom URL to load a DICOM image</div>
</div>
</div>
<div id="dicomInfo" class="dicom-info" style="display: none;">
<strong>DICOM Information:</strong><br>
<div id="dicomInfoContent"></div>
</div>
<div id="messages"></div>
</div>
<script>
let cornerstone = null;
let cornerstoneWADOImageLoader = null;
let dicomParser = null;
let isLoaded = false;
// Load sample URL
function loadSampleUrl(url) {
document.getElementById('customUrl').value = url;
loadDicomImage(url);
}
// Load custom URL
function loadCustomUrl() {
const url = document.getElementById('customUrl').value.trim();
if (url) {
loadDicomImage(url);
} else {
showMessage('Please enter a valid URL', 'error');
}
}
// Show message
function showMessage(message, type = 'info') {
const messagesDiv = document.getElementById('messages');
const messageDiv = document.createElement('div');
messageDiv.className = type;
messageDiv.textContent = message;
messagesDiv.appendChild(messageDiv);
// Remove message after 5 seconds
setTimeout(() => {
messageDiv.remove();
}, 5000);
}
// Update status
function updateStatus(message) {
document.getElementById('status').textContent = message;
}
// Show DICOM info
function showDicomInfo(info) {
const infoDiv = document.getElementById('dicomInfo');
const contentDiv = document.getElementById('dicomInfoContent');
if (info) {
contentDiv.innerHTML = info;
infoDiv.style.display = 'block';
} else {
infoDiv.style.display = 'none';
}
}
// Load libraries
async function loadLibraries() {
try {
updateStatus('Loading DICOM viewer libraries...');
// Load DICOM Parser first
await loadScript('https://unpkg.com/dicom-parser@1.8.6/dist/dicomParser.min.js');
dicomParser = window.dicomParser;
showMessage('DICOM Parser loaded successfully', 'success');
// Load Cornerstone Core
await loadScript('https://unpkg.com/cornerstone-core@2.3.0/dist/cornerstone.js');
cornerstone = window.cornerstone;
// Load Cornerstone WADO Image Loader with fallback
await loadCornerstoneWADO();
isLoaded = true;
updateStatus('Libraries loaded successfully');
showMessage('All DICOM viewer libraries loaded successfully', 'success');
// Initialize viewer
const element = document.getElementById('dicomImage');
cornerstone.enable(element);
} catch (error) {
updateStatus('Failed to load libraries');
showMessage(`Failed to load libraries: ${error.message}`, 'error');
}
}
// Load script
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.onload = resolve;
script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
document.head.appendChild(script);
});
}
// Load Cornerstone WADO Image Loader with fallback
function loadCornerstoneWADO() {
return new Promise((resolve, reject) => {
updateStatus('Loading Cornerstone WADO Image Loader...');
// Try multiple sources for WADO loader
const wadoSources = [
'https://unpkg.com/cornerstone-wado-image-loader@4.17.1/dist/cornerstoneWADOImageLoader.js',
'https://unpkg.com/cornerstone-wado-image-loader@4.16.0/dist/cornerstoneWADOImageLoader.js',
'https://unpkg.com/cornerstone-wado-image-loader@4.15.0/dist/cornerstoneWADOImageLoader.js',
'https://cdn.jsdelivr.net/npm/cornerstone-wado-image-loader@4.17.1/dist/cornerstoneWADOImageLoader.js'
];
let currentSourceIndex = 0;
function tryNextSource() {
if (currentSourceIndex >= wadoSources.length) {
reject(new Error('All WADO Image Loader sources failed'));
return;
}
const currentSource = wadoSources[currentSourceIndex];
updateStatus(`Trying WADO source ${currentSourceIndex + 1}: ${currentSource.split('/').pop()}`);
const wadoScript = document.createElement('script');
wadoScript.src = currentSource;
wadoScript.onload = () => {
try {
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
if (cornerstoneWADOImageLoader && cornerstone) {
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
showMessage(`WADO Image Loader loaded successfully from: ${currentSource.split('/').pop()}`, 'success');
resolve();
} else {
throw new Error('WADO loader not properly initialized');
}
} catch (error) {
showMessage(`WADO loader initialization failed: ${error.message}`, 'warning');
currentSourceIndex++;
tryNextSource();
}
};
wadoScript.onerror = (error) => {
showMessage(`Failed to load WADO from ${currentSource.split('/').pop()}`, 'warning');
currentSourceIndex++;
tryNextSource();
};
// Set timeout for loading
const timeout = setTimeout(() => {
showMessage(`WADO loader timeout from ${currentSource.split('/').pop()}`, 'warning');
currentSourceIndex++;
tryNextSource();
}, 8000);
wadoScript.onload = () => {
clearTimeout(timeout);
try {
cornerstoneWADOImageLoader = window.cornerstoneWADOImageLoader;
if (cornerstoneWADOImageLoader && cornerstone) {
cornerstoneWADOImageLoader.external.cornerstone = cornerstone;
showMessage(`WADO Image Loader loaded successfully from: ${currentSource.split('/').pop()}`, 'success');
resolve();
} else {
throw new Error('WADO loader not properly initialized');
}
} catch (error) {
showMessage(`WADO loader initialization failed: ${error.message}`, 'warning');
currentSourceIndex++;
tryNextSource();
}
};
document.head.appendChild(wadoScript);
}
tryNextSource();
});
}
// Load DICOM image
async function loadDicomImage(url) {
if (!isLoaded) {
showMessage('Libraries not loaded yet, please wait...', 'error');
return;
}
try {
updateStatus('Loading DICOM image...');
showMessage(`Loading DICOM image from: ${url}`, 'info');
// Test URL accessibility
await testUrl(url);
// Validate DICOM with parser
let dicomInfo = null;
try {
dicomInfo = await validateDicomWithParser(url);
showMessage('DICOM file validated successfully', 'success');
} catch (parserError) {
showMessage(`DICOM validation warning: ${parserError.message}`, 'warning');
}
const element = document.getElementById('dicomImage');
const image = await cornerstone.loadImage(`wadouri:${url}`);
cornerstone.displayImage(element, image);
updateStatus('DICOM image loaded successfully');
showMessage('DICOM image loaded successfully!', 'success');
// Display DICOM information if available
if (dicomInfo) {
displayDicomInfo(dicomInfo);
}
} catch (error) {
updateStatus('Failed to load DICOM image');
showMessage(`Failed to load DICOM image: ${error.message}`, 'error');
showDicomInfo(null);
}
}
// Validate DICOM with parser
async function validateDicomWithParser(url) {
try {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
if (!dicomParser) {
throw new Error('DICOM Parser not available');
}
const dataSet = dicomParser.parseDicom(arrayBuffer);
return dataSet;
} catch (error) {
throw new Error(`DICOM validation failed: ${error.message}`);
}
}
// Display DICOM information
function displayDicomInfo(dataSet) {
try {
const info = [
`Patient Name: ${dataSet.string('x00100010') || 'Unknown'}`,
`Patient ID: ${dataSet.string('x00100020') || 'Unknown'}`,
`Modality: ${dataSet.string('x00080060') || 'Unknown'}`,
`Study Date: ${dataSet.string('x00080020') || 'Unknown'}`,
`Study Description: ${dataSet.string('x00081030') || 'Unknown'}`,
`Manufacturer: ${dataSet.string('x00080070') || 'Unknown'}`,
`Image Size: ${dataSet.uint16('x00280010') || 'Unknown'} x ${dataSet.uint16('x00280011') || 'Unknown'}`,
`Bits Allocated: ${dataSet.uint16('x00280100') || 'Unknown'}`,
`Samples per Pixel: ${dataSet.uint16('x00280002') || 'Unknown'}`
].join('<br>');
showDicomInfo(info);
} catch (error) {
console.error('Error displaying DICOM info:', error);
showDicomInfo('Error displaying DICOM information');
}
}
// Test URL accessibility
async function testUrl(url) {
try {
const response = await fetch(url, { method: 'HEAD' });
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const contentType = response.headers.get('content-type');
console.log('Content-Type:', contentType);
if (contentType && !contentType.includes('application/dicom') && !contentType.includes('image/')) {
console.warn('Warning: Unexpected content type:', contentType);
}
} catch (error) {
throw new Error(`URL not accessible: ${error.message}`);
}
}
// Initialize when page loads
window.addEventListener('load', loadLibraries);
</script>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View File

@ -0,0 +1,249 @@
/*
* 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, AIPredictionDetailScreen } from '../screens';
import { ComingSoonScreen, DicomViewer } 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={() => <DicomViewer
dicomUrl={'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm'}
debugMode={true}
onError={(error) => console.log('DICOM Error:', error)}
onLoad={() => console.log('DICOM Viewer loaded successfully')}
/>}
options={({ navigation, route }) => ({
title: 'Create Suggestion',
headerLeft: () => (
<HeaderBackButton onPress={() => navigation.goBack()} />
),
headerRight: () => (
<HeaderActionButton
iconName="help-circle"
onPress={() => {
// Show help for suggestion form
console.log('Show help for case:', route.params?.caseId);
}}
accessibilityLabel="Help"
/>
),
})}
/>
{/* 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.
*/

View 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.
*/

View 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 with suggestion form
* - 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View 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.
*/

File diff suppressed because it is too large Load Diff

View 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.
*/

View File

@ -0,0 +1,15 @@
/*
* 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';
export { default as AIPredictionDetailScreen } from './AIPredictionDetailScreen';
/*
* End of File: index.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,337 @@
/*
* 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 }));
},
/**
* Submit AI Suggestion
*
* Purpose: Submit physician suggestions for AI findings
*
* @param suggestionData - Suggestion data including patient ID, type, title, text, etc.
* @param token - Authentication token
* @returns Promise with suggestion submission result
*/
submitAISuggestion: (suggestionData: {
patid: string;
suggestion_type: string;
suggestion_title: string;
suggestion_text: string;
confidence_score: number;
priority_level: string;
category: string;
related_findings: Record<string, string>;
evidence_sources: string[];
contraindications: string;
cost_estimate: number;
time_estimate: string;
expires_at: string | null;
tags: string[];
ai_model_version: string;
}, token: string) => {
return api.post('/api/ai-cases/suggestions', suggestionData, buildHeaders({ token }));
},
/**
* Get AI Suggestions
*
* Purpose: Fetch AI suggestions for a specific case or all suggestions
*
* @param token - Authentication token
* @param params - Optional query parameters
* @returns Promise with suggestions data
*/
getAISuggestions: (token: string, params?: {
caseId?: string;
patientId?: string;
suggestionType?: string;
priority?: string;
status?: string;
page?: number;
limit?: number;
}) => {
const queryParams = params ? { ...params } : {};
return api.get('/api/ai-cases/suggestions', queryParams, buildHeaders({ token }));
},
/**
* Update AI Suggestion
*
* Purpose: Update an existing AI suggestion
*
* @param suggestionId - Suggestion ID to update
* @param updateData - Updated suggestion data
* @param token - Authentication token
* @returns Promise with updated suggestion data
*/
updateAISuggestion: (suggestionId: string, updateData: {
suggestion_title?: string;
suggestion_text?: string;
priority_level?: string;
category?: string;
related_findings?: Record<string, string>;
evidence_sources?: string[];
contraindications?: string;
cost_estimate?: number;
time_estimate?: string;
expires_at?: string | null;
tags?: string[];
}, token: string) => {
return api.put(`/api/ai-cases/suggestions/${suggestionId}`, updateData, buildHeaders({ token }));
},
/**
* Delete AI Suggestion
*
* Purpose: Delete an AI suggestion
*
* @param suggestionId - Suggestion ID to delete
* @param token - Authentication token
* @returns Promise with deletion result
*/
deleteAISuggestion: (suggestionId: string, token: string) => {
return api.delete(`/api/ai-cases/suggestions/${suggestionId}`, {}, buildHeaders({ token }));
},
/**
* Get Suggestion Statistics
*
* Purpose: Fetch statistics about AI suggestions
*
* @param token - Authentication token
* @param timeRange - Optional time range filter
* @returns Promise with suggestion statistics
*/
getSuggestionStats: (token: string, timeRange?: 'today' | 'week' | 'month') => {
const params = timeRange ? { timeRange } : {};
return api.get('/api/ai-cases/suggestions/statistics', params, buildHeaders({ token }));
}
};
/*
* End of File: aiPredictionAPI.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View 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.
*/

View 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.
*/

View 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.
*/

View File

@ -28,6 +28,7 @@ import { theme } from '../../../../theme/theme';
import { DocumentUploadStepProps } from '../../types/signup';
import Icon from 'react-native-vector-icons/Feather';
import { showError, showSuccess } from '../../../../shared/utils/toast';
import { validateFileType, validateFileSize, formatFileSize } from '../../../../shared/utils/fileUpload';
// ============================================================================
// INTERFACES
@ -183,6 +184,17 @@ const DocumentUploadStep: React.FC<DocumentUploadStepProps> = ({
size: asset.fileSize,
};
// Validate file type and size
if (!validateFileType(imageData)) {
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
return;
}
if (!validateFileSize(imageData, 10)) {
showError('File Too Large', 'Please select an image under 10MB.');
return;
}
setSelectedImage(imageData);
showSuccess('Success', 'Document captured successfully!');
}
@ -221,6 +233,17 @@ const DocumentUploadStep: React.FC<DocumentUploadStepProps> = ({
size: asset.fileSize,
};
// Validate file type and size
if (!validateFileType(imageData)) {
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
return;
}
if (!validateFileSize(imageData, 10)) {
showError('File Too Large', 'Please select an image under 10MB.');
return;
}
setSelectedImage(imageData);
showSuccess('Success', 'Document selected from gallery!');
}
@ -231,23 +254,7 @@ const DocumentUploadStep: React.FC<DocumentUploadStepProps> = ({
// UTILITY FUNCTIONS
// ============================================================================
/**
* Format File Size
*
* Purpose: Convert bytes to human readable format
*
* @param bytes - File size in bytes
* @returns Formatted file size string
*/
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '';
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
};
/**
* Get File Type Display

View File

@ -163,7 +163,7 @@ const styles = StyleSheet.create({
textAlign: 'center',
},
subtitle: {
fontSize: theme.typography.bodyMedium,
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
textAlign: 'center',
},
@ -171,25 +171,25 @@ const styles = StyleSheet.create({
marginBottom: theme.spacing.xl,
},
message: {
fontSize: theme.typography.bodyMedium,
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.md,
fontFamily: theme.typography.fontFamily.regular,
},
optionsContainer: {
marginLeft: theme.spacing.sm,
},
optionText: {
fontSize: theme.typography.bodyMedium,
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
fontFamily: theme.typography.fontFamily.regular,
},
actions: {
flexDirection: 'row',
justifyContent: 'space-between',
flexDirection: 'column', // Changed from 'row' to 'column'
gap: theme.spacing.md,
},
secondaryButton: {
flex: 1,
backgroundColor: theme.colors.background,
borderWidth: 1,
borderColor: theme.colors.border,
@ -199,12 +199,11 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
secondaryButtonText: {
fontSize: theme.typography.bodyMedium,
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
},
primaryButton: {
flex: 1,
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
@ -217,7 +216,7 @@ const styles = StyleSheet.create({
elevation: 3,
},
primaryButtonText: {
fontSize: theme.typography.bodyMedium,
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},

View File

@ -6,9 +6,9 @@
*/
import { createAsyncThunk } from '@reduxjs/toolkit';
import { logout } from './authSlice';
import { logout, updateUserProfile } from './authSlice';
import { authAPI } from '../services/authAPI';
import { showError, showSuccess } from '../../../shared/utils/toast';
import { showError, showSuccess, showWarning } from '../../../shared/utils/toast';
/**
* Thunk to login user
@ -31,6 +31,10 @@ export const login = createAsyncThunk(
if (response.ok && response.data && response.data.data) {
// Return the user data for the fulfilled case
if(response.data.data.user.dashboard_role !=='radiologist'){
showWarning('You are not authorized to access this application')
return rejectWithValue('Not Authorized');
}
return {...response.data.data.user,access_token:response.data.data.access_token};
} else {
const errorMessage = response.data?.message || response.problem || 'Unknown error';
@ -42,6 +46,78 @@ export const login = createAsyncThunk(
}
);
/**
* Thunk to update user profile
*/
export const updateUserProfileAsync = createAsyncThunk(
'auth/updateUserProfile',
async (profileData: { first_name: string; last_name: string }, { getState, rejectWithValue, dispatch }) => {
try {
const state = getState() as any;
const user = state.auth.user;
const token = user?.access_token;
if (!user?.user_id || !token) {
return rejectWithValue('User not authenticated');
}
const response: any = await authAPI.updateUserProfile(user.user_id, profileData, token);
if (response.ok && response.data) {
// Update local state
dispatch(updateUserProfile({
first_name: profileData.first_name,
last_name: profileData.last_name,
display_name: `${profileData.first_name} ${profileData.last_name}`
}));
showSuccess('Profile updated successfully');
return response.data;
} else {
const errorMessage = response.data?.message || response.problem || 'Failed to update profile';
showError(errorMessage);
return rejectWithValue(errorMessage);
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to update profile';
showError(errorMessage);
return rejectWithValue(errorMessage);
}
}
);
/**
* Thunk to change password
*/
export const changePasswordAsync = createAsyncThunk(
'auth/changePassword',
async (passwordData: { currentPassword: string; newPassword: string }, { getState, rejectWithValue }) => {
try {
const state = getState() as any;
const user = state.auth.user;
const token = user?.access_token;
if (!user?.user_id || !token) {
return rejectWithValue('User not authenticated');
}
const response: any = await authAPI.changePassword(user.user_id, { password: passwordData.newPassword }, token);
if (response.ok && response.data) {
showSuccess('Password changed successfully');
return response.data;
} else {
const errorMessage = response.data?.message || response.problem || 'Failed to change password';
showError(errorMessage);
return rejectWithValue(errorMessage);
}
} catch (error: any) {
const errorMessage = error.message || 'Failed to change password';
showError(errorMessage);
return rejectWithValue(errorMessage);
}
}
);
/**
* Thunk to logout user

View File

@ -138,8 +138,8 @@ const LoginScreen: React.FC<LoginScreenProps> = ({ navigation }) => {
* HEADER SECTION - App branding and title
* ======================================================================== */}
<View style={styles.header}>
<Text style={styles.title}>Physician</Text>
<Text style={styles.subtitle}>Emergency Department Access</Text>
<Text style={styles.title}>Radiologist</Text>
{/* <Text style={styles.subtitle}>Emergency Department Access</Text> */}
</View>
<View style={styles.imageContainer}>
<Image source={require('../../../assets/images/hospital-logo.png')} style={styles.image} />

View File

@ -29,6 +29,7 @@ import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import { updateOnboarded, logout } from '../redux/authSlice';
import { authAPI } from '../services/authAPI';
import { showError, showSuccess } from '../../../shared/utils/toast';
import { validateFileType, validateFileSize, prepareFileForUpload } from '../../../shared/utils/fileUpload';
import { theme } from '../../../theme/theme';
import Icon from 'react-native-vector-icons/Feather';
import { AuthNavigationProp } from '../navigation/navigationTypes';
@ -364,6 +365,17 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
size: asset.fileSize,
};
// Validate file type and size
if (!validateFileType(imageData)) {
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
return;
}
if (!validateFileSize(imageData, 10)) {
showError('File Too Large', 'Please select an image under 10MB.');
return;
}
setSelectedImage(imageData);
showSuccess('Success', 'Document captured successfully!');
}
@ -402,6 +414,17 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
size: asset.fileSize,
};
// Validate file type and size
if (!validateFileType(imageData)) {
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
return;
}
if (!validateFileSize(imageData, 10)) {
showError('File Too Large', 'Please select an image under 10MB.');
return;
}
setSelectedImage(imageData);
showSuccess('Success', 'Document selected from gallery!');
}
@ -433,12 +456,11 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
try {
const formData = new FormData();
const file = {
uri: selectedImage.uri,
name: selectedImage.name,
type: selectedImage.type,
};
formData.append('id_photo', file as any);
// Prepare file with proper structure using utility function
const preparedFile = prepareFileForUpload(selectedImage, 'id_photo');
formData.append('id_photo', preparedFile as any);
const response: any = await authAPI.uploadDocument(formData, user?.access_token);
console.log('upload response',response)

View File

@ -40,6 +40,7 @@ import { selectHospitalLoading, selectHospitals } from '../redux/hospitalSelecto
import { SignUpData, SignUpStep } from '../types/signup';
import { authAPI } from '../services/authAPI';
import { showError, showSuccess } from '../../../shared/utils/toast';
import { createFormDataWithFile, validateFileType, validateFileSize } from '../../../shared/utils/fileUpload';
// ============================================================================
// INTERFACES
@ -225,26 +226,45 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ navigation }) => {
setIsLoading(true);
try {
const formData = new FormData();
let role = 'er_physician';
let role = 'radiologist';
formData.append('email', payload.email);
formData.append('password', payload.password);
formData.append('first_name', payload.first_name);
formData.append('last_name', payload.last_name);
formData.append('username', payload.username);
formData.append('dashboard_role', role);
formData.append('hospital_id', payload.hospital_id);
// Attach file if exists
// Prepare form data with proper file handling
const formFields = {
email: payload.email,
password: payload.password,
first_name: payload.first_name,
last_name: payload.last_name,
username: payload.username,
dashboard_role: role,
hospital_id: payload.hospital_id,
};
let formData: FormData;
// Handle file upload with validation
if (payload.id_photo_url) {
const filePath = payload.id_photo_url;
const file = {
uri: filePath,
name: 'id_photo',
type: 'image/jpg',
const fileData = {
uri: payload.id_photo_url,
name: `id_photo_${Date.now()}.jpg`,
type: 'image/jpeg',
};
formData.append('id_photo_url', file as any);
// Validate file type and size
if (!validateFileType(fileData)) {
showError('Invalid file type. Please select a JPEG, JPG, or PNG image.');
return;
}
if (!validateFileSize(fileData, 10)) {
showError('File size too large. Please select an image under 10MB.');
return;
}
// Create FormData with file
formData = createFormDataWithFile(formFields, fileData, 'id_photo_url');
} else {
// Create FormData without file
formData = createFormDataWithFile(formFields);
}
console.log('payload prepared', formData);

View File

@ -21,7 +21,11 @@ export const authAPI = {
//fetch hospital list
gethospitals: () => api.get('/api/hospitals/hospitals/app_user/hospitals', {},buildHeaders()),
//user signup
signup: (formData:any) => api.post('/api/auth/auth/admin/create-user-fromapp', formData,buildHeaders({ contentType: 'multipart/form-data' })),
signup: (formData:any) => api.post('/api/auth/auth/admin/create-user-fromapp', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
}),
//validate email
validatemail: (payload:{email:string}) => api.post('/api/auth/auth/check-email', payload,buildHeaders()),
//change password
@ -29,7 +33,32 @@ export const authAPI = {
//validate username
validateusername: (username:string|undefined) => api.post('/api/auth/auth/check-username', {username},buildHeaders()),
//upload document for onboarding
uploadDocument: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-id-photo', formData,buildHeaders({token:token, contentType: 'multipart/form-data' }))
uploadDocument: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-id-photo', formData, {
headers: {
'Content-Type': 'multipart/form-data',
...(token && { 'Authorization': `Bearer ${token}` }),
},
}),
// Update user profile
updateUserProfile: (userId: string, profileData: {
first_name: string;
last_name: string;
}, token: string) => api.put(
`/api/auth/auth/admin/users/self/${userId}`,
profileData,
buildHeaders({ token })
),
// Change password (admin endpoint)
changePassword: (userId: string, passwordData: {
password: string;
}, token: string) => api.put(
`/api/auth/auth/admin/users/self/${userId}`,
passwordData,
buildHeaders({ token })
),
// Add more endpoints as needed
};

View File

@ -365,7 +365,7 @@ const styles = StyleSheet.create({
// Header section
header: {
marginBottom: theme.spacing.lg,
// marginBottom: theme.spacing.lg,
},
// Main title
@ -465,10 +465,8 @@ const styles = StyleSheet.create({
// Pie chart container
pieChartContainer: {
alignItems: 'center',
marginBottom: theme.spacing.lg,
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
},
// Legend container
@ -478,7 +476,6 @@ const styles = StyleSheet.create({
backgroundColor: theme.colors.backgroundAlt,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginTop: theme.spacing.md,
},
// Legend title

View File

@ -29,33 +29,6 @@ export const DashboardHeader: React.FC<DashboardHeaderProps> = ({
{dashboard.shiftInfo.currentShift} Shift {dashboard.shiftInfo.attendingPhysician}
</Text>
</View>
<View style={styles.statsContainer}>
<View style={styles.statItem}>
<Text style={styles.statValue}>{dashboard.totalPatients}</Text>
<Text style={styles.statLabel}>Total Patients</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, styles.criticalValue]}>
{dashboard.criticalPatients}
</Text>
<Text style={styles.statLabel}>Critical</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{dashboard.pendingScans}</Text>
<Text style={styles.statLabel}>Pending Scans</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{dashboard.bedOccupancy}%</Text>
<Text style={styles.statLabel}>Bed Occupancy</Text>
</View>
</View>
<View style={styles.lastUpdated}>
<Text style={styles.lastUpdatedText}>
Last updated: {dashboard.lastUpdated.toLocaleTimeString()}
</Text>
</View>
</View>
);
};
@ -68,9 +41,6 @@ const styles = StyleSheet.create({
marginBottom: theme.spacing.lg,
...theme.shadows.medium,
},
header: {
marginBottom: theme.spacing.lg,
},
title: {
fontSize: theme.typography.fontSize.displayMedium,
fontFamily: theme.typography.fontFamily.bold,
@ -81,37 +51,7 @@ const styles = StyleSheet.create({
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
},
statsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: theme.spacing.lg,
},
statItem: {
alignItems: 'center',
flex: 1,
},
statValue: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.primary,
marginBottom: theme.spacing.xs,
},
criticalValue: {
color: theme.colors.critical,
},
statLabel: {
fontSize: theme.typography.fontSize.bodySmall,
color: theme.colors.textSecondary,
textAlign: 'center',
},
lastUpdated: {
alignItems: 'center',
},
lastUpdatedText: {
fontSize: theme.typography.fontSize.caption,
color: theme.colors.textMuted,
},
});
});
/*
* End of File: DashboardHeader.tsx

View File

@ -0,0 +1,337 @@
/*
* File: FeedbackAnalysisPieChart.tsx
* Description: Pie chart component for feedback analysis using react-native-chart-kit
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import { View, Text, StyleSheet, Dimensions } from 'react-native';
import { PieChart } from 'react-native-chart-kit';
import { theme } from '../../../theme/theme';
// ============================================================================
// TYPES
// ============================================================================
/**
* Feedback Analysis Data Interface
*
* Purpose: Defines the structure of feedback analysis data for pie chart
*/
interface FeedbackAnalysisData {
positive: number;
negative: number;
total: number;
}
/**
* FeedbackAnalysisPieChart Props Interface
*
* Purpose: Defines the props required by the FeedbackAnalysisPieChart component
*
* Props:
* - data: Feedback analysis data containing positive, negative, and total counts
* - title: Optional title for the chart
* - width: Chart width (defaults to screen width - 32)
* - height: Chart height (defaults to 220)
*/
interface FeedbackAnalysisPieChartProps {
data: FeedbackAnalysisData;
title?: string;
width?: number;
height?: number;
}
// ============================================================================
// COMPONENT
// ============================================================================
/**
* FeedbackAnalysisPieChart Component
*
* Purpose: Renders a pie chart showing feedback analysis distribution
*
* Features:
* - Pie chart visualization of positive vs negative feedback
* - Custom colors for different feedback types
* - Responsive sizing
* - Legend with percentages
* - Empty state handling
*/
export const FeedbackAnalysisPieChart: React.FC<FeedbackAnalysisPieChartProps> = ({
data,
title = 'Feedback Analysis Overview',
width = Dimensions.get('window').width - 32,
height = 220,
}) => {
// ============================================================================
// DATA PROCESSING
// ============================================================================
/**
* Process data for pie chart
*
* Purpose: Convert feedback data into chart-kit format
*/
const chartData = React.useMemo(() => {
const { positive, negative } = data;
// Only show data if there are actual feedbacks
if (positive === 0 && negative === 0) {
return [];
}
const chartDataArray = [];
// Add positive feedback data
if (positive > 0) {
chartDataArray.push({
name: 'Positive',
population: positive,
color: theme.colors.success,
legendFontColor: theme.colors.textPrimary,
legendFontSize: 12,
});
}
// Add negative feedback data
if (negative > 0) {
chartDataArray.push({
name: 'Negative',
population: negative,
color: theme.colors.error,
legendFontColor: theme.colors.textPrimary,
legendFontSize: 12,
});
}
return chartDataArray;
}, [data]);
// ============================================================================
// CHART CONFIGURATION
// ============================================================================
/**
* Chart configuration object
*
* Purpose: Configure pie chart appearance and behavior
*/
const chartConfig = {
backgroundColor: theme.colors.background,
backgroundGradientFrom: theme.colors.background,
backgroundGradientTo: theme.colors.background,
decimalPlaces: 0,
color: (opacity = 1) => theme.colors.primary,
labelColor: (opacity = 1) => theme.colors.textPrimary,
style: {
borderRadius: theme.borderRadius.medium,
},
propsForDots: {
r: '6',
strokeWidth: '2',
stroke: theme.colors.primary,
},
};
// ============================================================================
// RENDER FUNCTIONS
// ============================================================================
/**
* Render empty state
*
* Purpose: Show message when no feedback data is available
*/
const renderEmptyState = () => (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>No feedback data available</Text>
<Text style={styles.emptyStateSubtext}>
Feedback will appear here once received
</Text>
</View>
);
/**
* Render chart legend
*
* Purpose: Display custom legend with percentages
*/
const renderLegend = () => {
const { positive, negative, total } = data;
if (total === 0) return null;
const positivePercentage = ((positive / total) * 100).toFixed(1);
const negativePercentage = ((negative / total) * 100).toFixed(1);
return (
<View style={styles.legendContainer}>
<View style={styles.legendItem}>
<View style={[styles.legendColor, { backgroundColor: theme.colors.success }]} />
<Text style={styles.legendText}>
Positive: {positive} ({positivePercentage}%)
</Text>
</View>
<View style={styles.legendItem}>
<View style={[styles.legendColor, { backgroundColor: theme.colors.error }]} />
<Text style={styles.legendText}>
Negative: {negative} ({negativePercentage}%)
</Text>
</View>
<View style={styles.totalContainer}>
<Text style={styles.totalText}>
Total Feedback: {total}
</Text>
</View>
</View>
);
};
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<View style={styles.container}>
{/* Chart Title */}
{/* {title && (
<Text style={styles.title}>{title}</Text>
)} */}
{/* Chart Container */}
<View style={styles.chartContainer}>
{chartData.length > 0 ? (
<>
{/* Pie Chart */}
<PieChart
data={chartData}
width={width}
height={height}
chartConfig={chartConfig}
accessor="population"
backgroundColor="transparent"
paddingLeft="0"
center={[width/4, 0]}
absolute
hasLegend={false}
/>
{/* Custom Legend */}
{renderLegend()}
</>
) : (
renderEmptyState()
)}
</View>
</View>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
alignItems: 'center',
justifyContent: 'center',
minHeight: 250,
},
// Chart title styling
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.md,
textAlign: 'center',
},
// Chart container
chartContainer: {
alignItems: 'center',
justifyContent: 'center',
width: '100%',
flex: 1,
},
// Empty state styling
emptyState: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: theme.spacing.xl,
minHeight: 150,
},
// Empty state text styling
emptyStateText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Empty state subtext styling
emptyStateSubtext: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
textAlign: 'center',
},
// Legend container styling
legendContainer: {
marginTop: theme.spacing.md,
alignItems: 'center',
},
// Legend item styling
legendItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: theme.spacing.sm,
},
// Legend color indicator styling
legendColor: {
width: 16,
height: 16,
borderRadius: 8,
marginRight: theme.spacing.sm,
},
// Legend text styling
legendText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
},
// Total container styling
totalContainer: {
marginTop: theme.spacing.sm,
paddingTop: theme.spacing.sm,
borderTopWidth: 1,
borderTopColor: theme.colors.border,
alignItems: 'center',
},
// Total text styling
totalText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
},
});
/*
* End of File: FeedbackAnalysisPieChart.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -3,4 +3,5 @@ export { CriticalAlerts } from './CriticalAlerts';
export { DashboardHeader } from './DashboardHeader';
export { QuickActions } from './QuickActions';
export { DepartmentStats } from './DepartmentStats';
export { BrainPredictionsOverview } from './BrainPredictionsOverview';
export { BrainPredictionsOverview } from './BrainPredictionsOverview';
export { FeedbackAnalysisPieChart } from './FeedbackAnalysisPieChart';

View File

@ -0,0 +1,14 @@
/*
* File: index.ts
* Description: Dashboard hooks exports
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export * from './useAIDashboard';
/*
* End of File: index.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,101 @@
/*
* File: useAIDashboard.ts
* Description: Custom hook for AI dashboard functionality
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { AppDispatch } from '../../../store';
import { selectAIDashboardData, selectAIDashboardError, selectAIDashboardLoading, selectAIDashboardRefreshing, selectDashboardMessage } from '../redux/aiDashboardSelectors';
import { fetchAIDashboardStatistics, refreshAIDashboardStatistics } from '../redux/aiDashboardSlice';
import { selectUser } from '../../Auth/redux';
// import {
// fetchAIDashboardStatistics,
// refreshAIDashboardStatistics,
// selectAIDashboardData,
// selectAIDashboardLoading,
// selectAIDashboardRefreshing,
// selectAIDashboardError,
// selectDashboardMessage
// } from '../redux';
/**
* useAIDashboard Custom Hook
*
* Purpose: Custom hook for AI dashboard functionality
*
* Features:
* - Fetch dashboard statistics from API
* - Refresh dashboard data
* - Access dashboard state from Redux
* - Handle authentication token
*
* @returns Object containing dashboard state and actions
*/
export const useAIDashboard = () => {
const dispatch = useDispatch<AppDispatch>();
// Select dashboard data from Redux store
const dashboardData = useSelector(selectAIDashboardData);
const isLoading = useSelector(selectAIDashboardLoading);
const isRefreshing = useSelector(selectAIDashboardRefreshing);
const error = useSelector(selectAIDashboardError);
const dashboardMessage = useSelector(selectDashboardMessage);
// TODO: Get actual authentication token from auth store
// For now, using a placeholder token
const authToken = useSelector(selectUser)?.access_token;
/**
* Fetch Dashboard Statistics
*
* Purpose: Fetch dashboard statistics from API
*/
const fetchDashboardStatistics = () => {
dispatch(fetchAIDashboardStatistics(authToken));
};
/**
* Refresh Dashboard Statistics
*
* Purpose: Refresh dashboard statistics from API
*/
const refreshDashboardStatistics = () => {
dispatch(refreshAIDashboardStatistics(authToken));
};
/**
* useEffect for initial data loading
*
* Purpose: Load initial dashboard data from API when hook is used
*/
useEffect(() => {
// Fetch dashboard statistics from API
fetchDashboardStatistics();
}, []);
return {
// State
dashboardData,
isLoading,
isRefreshing,
error,
dashboardMessage,
// Actions
fetchDashboardStatistics,
refreshDashboardStatistics,
// Constants
authToken
};
};
/*
* End of File: useAIDashboard.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -6,7 +6,7 @@
*/
// Export screens
export { default as ERDashboardScreen } from './screens/ERDashboardScreen';
export { default as DashboardScreen } from './screens/DashboardScreen';
// Export navigation
export {
@ -14,7 +14,7 @@ export {
DashboardStackParamList,
DashboardNavigationProp,
DashboardScreenProps,
ERDashboardScreenProps,
DashboardScreenProps,
PatientDetailsScreenProps,
AlertDetailsScreenProps,
DepartmentStatsScreenProps,
@ -39,6 +39,9 @@ export { default as DashboardHeader } from './components/DashboardHeader';
export { default as QuickActions } from './components/QuickActions';
export { default as DepartmentStats } from './components/DepartmentStats';
// Export hooks
export * from './hooks';
// Export Redux
export {
fetchDashboardData,
@ -51,6 +54,36 @@ export {
updateDashboardData,
} from './redux/dashboardSlice';
// Export AI Dashboard Redux
export {
fetchAIDashboardStatistics,
refreshAIDashboardStatistics,
clearError as clearAIDashboardError,
setTimeRange,
setHospital,
setDepartment,
updateDashboardData as updateAIDashboardData,
} from './redux/aiDashboardSlice';
// Export AI Dashboard Selectors
export {
selectAIDashboardData,
selectAIDashboardLoading,
selectAIDashboardRefreshing,
selectAIDashboardError,
selectDashboardMessage,
selectTotalPredictions,
selectTotalPatients,
selectTotalFeedbacks,
selectFeedbackRatePercentage,
selectAverageConfidenceScore,
selectCriticalCasePercentage,
selectConfidenceScores,
selectUrgencyLevels,
selectFeedbackAnalysis,
selectTimeAnalysis,
} from './redux/aiDashboardSelectors';
export {
fetchAlerts,
acknowledgeAlert,

View File

@ -9,7 +9,7 @@ import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
// Import dashboard screens
import { ERDashboardScreen } from '../screens/ERDashboardScreen';
import { DashboardScreen } from '../screens/DashboardScreen';
// Import navigation types
import { DashboardStackParamList } from './navigationTypes';
@ -22,7 +22,7 @@ const Stack = createStackNavigator<DashboardStackParamList>();
* DashboardStackNavigator - Manages navigation between dashboard screens
*
* This navigator handles the flow between:
* - ERDashboardScreen: Main ER dashboard with patient overview
* - DashboardScreen: Main ER dashboard with patient overview
* - Future screens: Patient details, alerts, reports, etc.
*
* Features:
@ -72,7 +72,7 @@ const DashboardStackNavigator: React.FC = () => {
{/* ER Dashboard Screen - Main dashboard entry point */}
<Stack.Screen
name="ERDashboard"
component={ERDashboardScreen}
component={DashboardScreen}
options={{
title: 'ER Dashboard',
headerShown: false, // Hide header for main dashboard

View File

@ -13,12 +13,12 @@ export type {
DashboardStackParamList,
DashboardNavigationProp,
DashboardScreenProps,
ERDashboardScreenProps,
DashboardScreenProps,
PatientDetailsScreenProps,
AlertDetailsScreenProps,
DepartmentStatsScreenProps,
QuickActionsScreenProps,
ERDashboardScreenParams,
DashboardScreenParams,
PatientDetailsScreenParams,
AlertDetailsScreenParams,
DepartmentStatsScreenParams,

View File

@ -16,7 +16,7 @@ import { Patient, Alert as AlertType, ERDashboard } from '../../../shared/types'
*/
export type DashboardStackParamList = {
// ER Dashboard screen - Main dashboard with patient overview
ERDashboard: ERDashboardScreenParams;
ERDashboard: DashboardScreenParams;
// Patient Details screen - Detailed patient information
PatientDetails: PatientDetailsScreenParams;
@ -59,7 +59,7 @@ export interface DashboardScreenProps<T extends keyof DashboardStackParamList> {
// ============================================================================
/**
* ERDashboardScreenParams
* DashboardScreenParams
*
* Purpose: Parameters passed to the ER dashboard screen
*
@ -67,7 +67,7 @@ export interface DashboardScreenProps<T extends keyof DashboardStackParamList> {
* - filter: Optional filter to apply to dashboard data
* - refresh: Optional flag to force refresh
*/
export interface ERDashboardScreenParams {
export interface DashboardScreenParams {
filter?: 'all' | 'critical' | 'active' | 'pending';
refresh?: boolean;
}
@ -140,9 +140,9 @@ export interface QuickActionsScreenParams {
// ============================================================================
/**
* ERDashboardScreenProps - Props for ERDashboardScreen component
* DashboardScreenProps - Props for DashboardScreen component
*/
export type ERDashboardScreenProps = DashboardScreenProps<'ERDashboard'>;
export type DashboardScreenProps = DashboardScreenProps<'ERDashboard'>;
/**
* PatientDetailsScreenProps - Props for PatientDetailsScreen component

Some files were not shown because too many files have changed in this diff Show More