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