Compare commits
No commits in common. "8fe78a6d7c07705f2159f73925bc1ad3aef48521" and "848bd3ea934746b41b75b2314cf273db34f58e6d" have entirely different histories.
8fe78a6d7c
...
848bd3ea93
@ -7,7 +7,7 @@ alwaysApply: true
|
|||||||
|
|
||||||
### 1. Root Level Organization
|
### 1. Root Level Organization
|
||||||
```
|
```
|
||||||
NeoScan_Radiologist/
|
NeoScan_Physician/
|
||||||
├── app/ # Main application code
|
├── app/ # Main application code
|
||||||
├── docs/ # Documentation
|
├── docs/ # Documentation
|
||||||
├── android/ # Android native code
|
├── android/ # Android native code
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
## 📁 Complete Directory Structure
|
## 📁 Complete Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
NeoScan_Radiologist/
|
NeoScan_Physician/
|
||||||
├── app/ # Main application code
|
├── app/ # Main application code
|
||||||
│ ├── modules/ # Feature-based modules
|
│ ├── modules/ # Feature-based modules
|
||||||
│ │ ├── Auth/ # Authentication module
|
│ │ ├── Auth/ # Authentication module
|
||||||
@ -29,7 +29,7 @@ NeoScan_Radiologist/
|
|||||||
│ │ │ │ ├── QuickActions.tsx # Emergency quick actions
|
│ │ │ │ ├── QuickActions.tsx # Emergency quick actions
|
||||||
│ │ │ │ └── DepartmentStats.tsx # Department statistics
|
│ │ │ │ └── DepartmentStats.tsx # Department statistics
|
||||||
│ │ │ ├── screens/ # Dashboard screens
|
│ │ │ ├── screens/ # Dashboard screens
|
||||||
│ │ │ │ └── DashboardScreen.tsx # Main ER dashboard
|
│ │ │ │ └── ERDashboardScreen.tsx # Main ER dashboard
|
||||||
│ │ │ ├── hooks/ # Dashboard custom hooks
|
│ │ │ ├── hooks/ # Dashboard custom hooks
|
||||||
│ │ │ ├── redux/ # Dashboard state management
|
│ │ │ ├── redux/ # Dashboard state management
|
||||||
│ │ │ ├── services/ # Dashboard API services
|
│ │ │ ├── services/ # Dashboard API services
|
||||||
@ -150,7 +150,7 @@ NeoScan_Radiologist/
|
|||||||
│ │ ├── AndroidManifest.xml # Main manifest
|
│ │ ├── AndroidManifest.xml # Main manifest
|
||||||
│ │ ├── java/ # Java source
|
│ │ ├── java/ # Java source
|
||||||
│ │ │ └── com/ # Package structure
|
│ │ │ └── com/ # Package structure
|
||||||
│ │ │ └── neoscan_radiologist/
|
│ │ │ └── neoscan_physician/
|
||||||
│ │ │ ├── MainActivity.kt # Main activity
|
│ │ │ ├── MainActivity.kt # Main activity
|
||||||
│ │ │ └── MainApplication.kt # Application class
|
│ │ │ └── MainApplication.kt # Application class
|
||||||
│ │ └── res/ # Resources
|
│ │ └── res/ # Resources
|
||||||
@ -166,7 +166,7 @@ NeoScan_Radiologist/
|
|||||||
│ ├── gradlew.bat # Windows gradle wrapper
|
│ ├── gradlew.bat # Windows gradle wrapper
|
||||||
│ └── settings.gradle # Gradle settings
|
│ └── settings.gradle # Gradle settings
|
||||||
├── ios/ # iOS native code
|
├── ios/ # iOS native code
|
||||||
│ ├── NeoScan_Radiologist/ # iOS app
|
│ ├── NeoScan_Physician/ # iOS app
|
||||||
│ │ ├── AppDelegate.swift # App delegate
|
│ │ ├── AppDelegate.swift # App delegate
|
||||||
│ │ ├── Images.xcassets/ # Image assets
|
│ │ ├── Images.xcassets/ # Image assets
|
||||||
│ │ │ ├── AppIcon.appiconset/ # App icons
|
│ │ │ ├── AppIcon.appiconset/ # App icons
|
||||||
@ -174,11 +174,11 @@ NeoScan_Radiologist/
|
|||||||
│ │ ├── Info.plist # App info
|
│ │ ├── Info.plist # App info
|
||||||
│ │ ├── LaunchScreen.storyboard # Launch screen
|
│ │ ├── LaunchScreen.storyboard # Launch screen
|
||||||
│ │ └── PrivacyInfo.xcprivacy # Privacy info
|
│ │ └── PrivacyInfo.xcprivacy # Privacy info
|
||||||
│ ├── NeoScan_Radiologist.xcodeproj/ # Xcode project
|
│ ├── NeoScan_Physician.xcodeproj/ # Xcode project
|
||||||
│ │ ├── project.pbxproj # Project file
|
│ │ ├── project.pbxproj # Project file
|
||||||
│ │ └── xcshareddata/ # Shared data
|
│ │ └── xcshareddata/ # Shared data
|
||||||
│ │ └── xcschemes/ # Build schemes
|
│ │ └── xcschemes/ # Build schemes
|
||||||
│ │ └── NeoScan_Radiologist.xcscheme
|
│ │ └── NeoScan_Physician.xcscheme
|
||||||
│ └── Podfile # CocoaPods configuration
|
│ └── Podfile # CocoaPods configuration
|
||||||
├── __tests__/ # Test files
|
├── __tests__/ # Test files
|
||||||
│ ├── App.test.tsx # App component tests
|
│ ├── App.test.tsx # App component tests
|
||||||
@ -223,7 +223,7 @@ NeoScan_Radiologist/
|
|||||||
|
|
||||||
### Dashboard Module
|
### Dashboard Module
|
||||||
**Purpose**: Main ER dashboard with patient monitoring and alerts
|
**Purpose**: Main ER dashboard with patient monitoring and alerts
|
||||||
- **DashboardScreen**: Main dashboard with patient list and statistics
|
- **ERDashboardScreen**: Main dashboard with patient list and statistics
|
||||||
- **PatientCard**: Individual patient information display
|
- **PatientCard**: Individual patient information display
|
||||||
- **CriticalAlerts**: High-priority alert notifications
|
- **CriticalAlerts**: High-priority alert notifications
|
||||||
- **QuickActions**: Emergency procedure shortcuts
|
- **QuickActions**: Emergency procedure shortcuts
|
||||||
|
|||||||
@ -42,7 +42,7 @@ A comprehensive React Native application designed for emergency department physi
|
|||||||
|
|
||||||
### Project Structure
|
### Project Structure
|
||||||
```
|
```
|
||||||
NeoScan_Radiologist/
|
NeoScan_Physician/
|
||||||
├── app/ # Main application code
|
├── app/ # Main application code
|
||||||
│ ├── modules/ # Feature-based modules
|
│ ├── modules/ # Feature-based modules
|
||||||
│ │ ├── Auth/ # Authentication module
|
│ │ ├── Auth/ # Authentication module
|
||||||
@ -120,7 +120,7 @@ NeoScan_Radiologist/
|
|||||||
1. **Clone the repository**
|
1. **Clone the repository**
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd NeoScan_Radiologist
|
cd NeoScan_Physician
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install dependencies**
|
2. **Install dependencies**
|
||||||
|
|||||||
@ -71,7 +71,6 @@ def enableProguardInReleaseBuilds = false
|
|||||||
* give correct results when using with locales other than en-US. Note that
|
* give correct results when using with locales other than en-US. Note that
|
||||||
* this variant is about 6MiB larger per architecture than default.
|
* this variant is about 6MiB larger per architecture than default.
|
||||||
*/
|
*/
|
||||||
def enableSeparateBuildPerCPUArchitecture = true
|
|
||||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@ -79,16 +78,9 @@ android {
|
|||||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||||
compileSdk rootProject.ext.compileSdkVersion
|
compileSdk rootProject.ext.compileSdkVersion
|
||||||
|
|
||||||
namespace "com.neoscan_radiologist"
|
namespace "com.neoscan_physician"
|
||||||
splits {
|
|
||||||
abi {
|
|
||||||
enable true
|
|
||||||
include 'armeabi-v7a', 'arm64-v8a', 'x86'
|
|
||||||
universalApk false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.neoscan_radiologist"
|
applicationId "com.neoscan_physician"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1
|
versionCode 1
|
||||||
|
|||||||
BIN
android/app/src/main/assets/fonts/Roboto-Black.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Roboto-Bold.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Roboto-ExtraBold.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Roboto-ExtraLight.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Roboto-Light.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Roboto-Medium.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Roboto-Regular.ttf
Normal file
BIN
android/app/src/main/assets/fonts/Roboto-SemiBold.ttf
Normal file
@ -1,4 +1,4 @@
|
|||||||
package com.neoscan_radiologist
|
package com.neoscan_physician
|
||||||
|
|
||||||
import com.facebook.react.ReactActivity
|
import com.facebook.react.ReactActivity
|
||||||
import com.facebook.react.ReactActivityDelegate
|
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
|
* Returns the name of the main component registered from JavaScript. This is used to schedule
|
||||||
* rendering of the component.
|
* rendering of the component.
|
||||||
*/
|
*/
|
||||||
override fun getMainComponentName(): String = "NeoScan_Radiologist"
|
override fun getMainComponentName(): String = "NeoScan_Physician"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package com.neoscan_radiologist
|
package com.neoscan_physician
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.facebook.react.PackageList
|
import com.facebook.react.PackageList
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 121 KiB |
@ -1,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Radiologist</string>
|
<string name="app_name">NeoScanPhysician</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@ -32,7 +32,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
|
|||||||
# your application. You should enable this flag either if you want
|
# your application. You should enable this flag either if you want
|
||||||
# to write custom TurboModules/Fabric components OR use libraries that
|
# to write custom TurboModules/Fabric components OR use libraries that
|
||||||
# are providing them.
|
# are providing them.
|
||||||
newArchEnabled=false
|
newArchEnabled=true
|
||||||
|
|
||||||
# Use this property to enable or disable the Hermes JS engine.
|
# Use this property to enable or disable the Hermes JS engine.
|
||||||
# If set to false, you will be using JSC instead.
|
# If set to false, you will be using JSC instead.
|
||||||
|
|||||||
@ -2,36 +2,36 @@
|
|||||||
"migIndex": 1,
|
"migIndex": 1,
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"path": "app/assets/fonts/WorkSans-Bold.ttf",
|
"path": "app/assets/fonts/Roboto-Black.ttf",
|
||||||
"sha1": "ec84061651ead3c3c5cbb61c2d338aca0bacdc1e"
|
"sha1": "d1678489a8d5645f16486ec52d77b651ff0bf327"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "app/assets/fonts/WorkSans-ExtraBold.ttf",
|
"path": "app/assets/fonts/Roboto-Bold.ttf",
|
||||||
"sha1": "0b371d1dbfbdd15db880bbd129b239530c71accb"
|
"sha1": "508c35dee818addce6cc6d1fb6e42f039da5a7cf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "app/assets/fonts/WorkSans-ExtraLight.ttf",
|
"path": "app/assets/fonts/Roboto-ExtraBold.ttf",
|
||||||
"sha1": "74596e55487e2961b6c43993698d658e2ceee77b"
|
"sha1": "3dbfd71b6fbcfbd8e7ee8a8dd033dc5aaad63249"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "app/assets/fonts/WorkSans-Light.ttf",
|
"path": "app/assets/fonts/Roboto-ExtraLight.ttf",
|
||||||
"sha1": "293e11dae7e8b930bf5eea0b06ca979531f22189"
|
"sha1": "df556e64732e5c272349e13cb5f87591a1ae779b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "app/assets/fonts/WorkSans-Medium.ttf",
|
"path": "app/assets/fonts/Roboto-Light.ttf",
|
||||||
"sha1": "c281f8454dd193c2260e43ae2de171c5dd4086e4"
|
"sha1": "318b44c0a32848f78bf11d4fbf3355d00647a796"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "app/assets/fonts/WorkSans-Regular.ttf",
|
"path": "app/assets/fonts/Roboto-Medium.ttf",
|
||||||
"sha1": "5e0183b29b57c54595c62ac6bc223b21f1434226"
|
"sha1": "fa5192203f85ddb667579e1bdf26f12098bb873b"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "app/assets/fonts/WorkSans-SemiBold.ttf",
|
"path": "app/assets/fonts/Roboto-Regular.ttf",
|
||||||
"sha1": "64b8fe156fafce221a0f66504255257053fc6062"
|
"sha1": "3bff51436aa7eb995d84cfc592cc63e1316bb400"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "app/assets/fonts/WorkSans-Thin.ttf",
|
"path": "app/assets/fonts/Roboto-SemiBold.ttf",
|
||||||
"sha1": "a62251331038fdd079c47bc413a350efbf702db8"
|
"sha1": "9ca139684fe902c8310dd82991648376ac9838db"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") }
|
||||||
plugins { id("com.facebook.react.settings") }
|
plugins { id("com.facebook.react.settings") }
|
||||||
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
|
||||||
rootProject.name = 'NeoScan_Radiologist'
|
rootProject.name = 'NeoScan_Physician'
|
||||||
include ':app'
|
include ':app'
|
||||||
includeBuild('../node_modules/@react-native/gradle-plugin')
|
includeBuild('../node_modules/@react-native/gradle-plugin')
|
||||||
|
|||||||
4
app.json
@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "NeoScan_Radiologist",
|
"name": "NeoScan_Physician",
|
||||||
"displayName": "NeoScan_Radiologist"
|
"displayName": "NeoScan_Physician"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,726 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@ -1,443 +0,0 @@
|
|||||||
<!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/Roboto-Black.ttf
Normal file
BIN
app/assets/fonts/Roboto-Bold.ttf
Normal file
BIN
app/assets/fonts/Roboto-ExtraBold.ttf
Normal file
BIN
app/assets/fonts/Roboto-ExtraLight.ttf
Normal file
BIN
app/assets/fonts/Roboto-Light.ttf
Normal file
BIN
app/assets/fonts/Roboto-Medium.ttf
Normal file
BIN
app/assets/fonts/Roboto-Regular.ttf
Normal file
BIN
app/assets/fonts/Roboto-SemiBold.ttf
Normal file
@ -1,249 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,361 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,231 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,522 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,287 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,368 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,139 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,226 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,454 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,383 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,249 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,251 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,410 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,621 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,749 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,337 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,221 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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,7 +28,6 @@ import { theme } from '../../../../theme/theme';
|
|||||||
import { DocumentUploadStepProps } from '../../types/signup';
|
import { DocumentUploadStepProps } from '../../types/signup';
|
||||||
import Icon from 'react-native-vector-icons/Feather';
|
import Icon from 'react-native-vector-icons/Feather';
|
||||||
import { showError, showSuccess } from '../../../../shared/utils/toast';
|
import { showError, showSuccess } from '../../../../shared/utils/toast';
|
||||||
import { validateFileType, validateFileSize, formatFileSize } from '../../../../shared/utils/fileUpload';
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// INTERFACES
|
// INTERFACES
|
||||||
@ -184,17 +183,6 @@ const DocumentUploadStep: React.FC<DocumentUploadStepProps> = ({
|
|||||||
size: asset.fileSize,
|
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);
|
setSelectedImage(imageData);
|
||||||
showSuccess('Success', 'Document captured successfully!');
|
showSuccess('Success', 'Document captured successfully!');
|
||||||
}
|
}
|
||||||
@ -233,17 +221,6 @@ const DocumentUploadStep: React.FC<DocumentUploadStepProps> = ({
|
|||||||
size: asset.fileSize,
|
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);
|
setSelectedImage(imageData);
|
||||||
showSuccess('Success', 'Document selected from gallery!');
|
showSuccess('Success', 'Document selected from gallery!');
|
||||||
}
|
}
|
||||||
@ -254,7 +231,23 @@ const DocumentUploadStep: React.FC<DocumentUploadStepProps> = ({
|
|||||||
// UTILITY FUNCTIONS
|
// 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
|
* Get File Type Display
|
||||||
|
|||||||
@ -163,7 +163,7 @@ const styles = StyleSheet.create({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: theme.typography.fontSize.bodyMedium,
|
fontSize: theme.typography.bodyMedium,
|
||||||
color: theme.colors.textSecondary,
|
color: theme.colors.textSecondary,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
@ -171,25 +171,25 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: theme.spacing.xl,
|
marginBottom: theme.spacing.xl,
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
fontSize: theme.typography.fontSize.bodyMedium,
|
fontSize: theme.typography.bodyMedium,
|
||||||
color: theme.colors.textPrimary,
|
color: theme.colors.textPrimary,
|
||||||
marginBottom: theme.spacing.md,
|
marginBottom: theme.spacing.md,
|
||||||
fontFamily: theme.typography.fontFamily.regular,
|
|
||||||
},
|
},
|
||||||
optionsContainer: {
|
optionsContainer: {
|
||||||
marginLeft: theme.spacing.sm,
|
marginLeft: theme.spacing.sm,
|
||||||
},
|
},
|
||||||
optionText: {
|
optionText: {
|
||||||
fontSize: theme.typography.fontSize.bodyMedium,
|
fontSize: theme.typography.bodyMedium,
|
||||||
color: theme.colors.textSecondary,
|
color: theme.colors.textSecondary,
|
||||||
marginBottom: theme.spacing.xs,
|
marginBottom: theme.spacing.xs,
|
||||||
fontFamily: theme.typography.fontFamily.regular,
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
flexDirection: 'column', // Changed from 'row' to 'column'
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
gap: theme.spacing.md,
|
gap: theme.spacing.md,
|
||||||
},
|
},
|
||||||
secondaryButton: {
|
secondaryButton: {
|
||||||
|
flex: 1,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
@ -199,11 +199,12 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
secondaryButtonText: {
|
secondaryButtonText: {
|
||||||
fontSize: theme.typography.fontSize.bodyMedium,
|
fontSize: theme.typography.bodyMedium,
|
||||||
fontFamily: theme.typography.fontFamily.bold,
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
color: theme.colors.textPrimary,
|
color: theme.colors.textPrimary,
|
||||||
},
|
},
|
||||||
primaryButton: {
|
primaryButton: {
|
||||||
|
flex: 1,
|
||||||
backgroundColor: theme.colors.primary,
|
backgroundColor: theme.colors.primary,
|
||||||
borderRadius: theme.borderRadius.medium,
|
borderRadius: theme.borderRadius.medium,
|
||||||
paddingVertical: theme.spacing.md,
|
paddingVertical: theme.spacing.md,
|
||||||
@ -216,7 +217,7 @@ const styles = StyleSheet.create({
|
|||||||
elevation: 3,
|
elevation: 3,
|
||||||
},
|
},
|
||||||
primaryButtonText: {
|
primaryButtonText: {
|
||||||
fontSize: theme.typography.fontSize.bodyMedium,
|
fontSize: theme.typography.bodyMedium,
|
||||||
fontFamily: theme.typography.fontFamily.bold,
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
color: theme.colors.background,
|
color: theme.colors.background,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { logout, updateUserProfile } from './authSlice';
|
import { logout } from './authSlice';
|
||||||
import { authAPI } from '../services/authAPI';
|
import { authAPI } from '../services/authAPI';
|
||||||
import { showError, showSuccess, showWarning } from '../../../shared/utils/toast';
|
import { showError, showSuccess } from '../../../shared/utils/toast';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thunk to login user
|
* Thunk to login user
|
||||||
@ -31,10 +31,6 @@ export const login = createAsyncThunk(
|
|||||||
|
|
||||||
if (response.ok && response.data && response.data.data) {
|
if (response.ok && response.data && response.data.data) {
|
||||||
// Return the user data for the fulfilled case
|
// 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};
|
return {...response.data.data.user,access_token:response.data.data.access_token};
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = response.data?.message || response.problem || 'Unknown error';
|
const errorMessage = response.data?.message || response.problem || 'Unknown error';
|
||||||
@ -46,78 +42,6 @@ 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
|
* Thunk to logout user
|
||||||
|
|||||||
@ -138,8 +138,8 @@ const LoginScreen: React.FC<LoginScreenProps> = ({ navigation }) => {
|
|||||||
* HEADER SECTION - App branding and title
|
* HEADER SECTION - App branding and title
|
||||||
* ======================================================================== */}
|
* ======================================================================== */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.title}>Radiologist</Text>
|
<Text style={styles.title}>Physician</Text>
|
||||||
{/* <Text style={styles.subtitle}>Emergency Department Access</Text> */}
|
<Text style={styles.subtitle}>Emergency Department Access</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.imageContainer}>
|
<View style={styles.imageContainer}>
|
||||||
<Image source={require('../../../assets/images/hospital-logo.png')} style={styles.image} />
|
<Image source={require('../../../assets/images/hospital-logo.png')} style={styles.image} />
|
||||||
|
|||||||
@ -29,7 +29,6 @@ import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
|||||||
import { updateOnboarded, logout } from '../redux/authSlice';
|
import { updateOnboarded, logout } from '../redux/authSlice';
|
||||||
import { authAPI } from '../services/authAPI';
|
import { authAPI } from '../services/authAPI';
|
||||||
import { showError, showSuccess } from '../../../shared/utils/toast';
|
import { showError, showSuccess } from '../../../shared/utils/toast';
|
||||||
import { validateFileType, validateFileSize, prepareFileForUpload } from '../../../shared/utils/fileUpload';
|
|
||||||
import { theme } from '../../../theme/theme';
|
import { theme } from '../../../theme/theme';
|
||||||
import Icon from 'react-native-vector-icons/Feather';
|
import Icon from 'react-native-vector-icons/Feather';
|
||||||
import { AuthNavigationProp } from '../navigation/navigationTypes';
|
import { AuthNavigationProp } from '../navigation/navigationTypes';
|
||||||
@ -365,17 +364,6 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
|||||||
size: asset.fileSize,
|
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);
|
setSelectedImage(imageData);
|
||||||
showSuccess('Success', 'Document captured successfully!');
|
showSuccess('Success', 'Document captured successfully!');
|
||||||
}
|
}
|
||||||
@ -414,17 +402,6 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
|||||||
size: asset.fileSize,
|
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);
|
setSelectedImage(imageData);
|
||||||
showSuccess('Success', 'Document selected from gallery!');
|
showSuccess('Success', 'Document selected from gallery!');
|
||||||
}
|
}
|
||||||
@ -456,11 +433,12 @@ export const ResetPasswordScreen: React.FC<ResetPasswordScreenProps> = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
const file = {
|
||||||
// Prepare file with proper structure using utility function
|
uri: selectedImage.uri,
|
||||||
const preparedFile = prepareFileForUpload(selectedImage, 'id_photo');
|
name: selectedImage.name,
|
||||||
|
type: selectedImage.type,
|
||||||
formData.append('id_photo', preparedFile as any);
|
};
|
||||||
|
formData.append('id_photo', file as any);
|
||||||
|
|
||||||
const response: any = await authAPI.uploadDocument(formData, user?.access_token);
|
const response: any = await authAPI.uploadDocument(formData, user?.access_token);
|
||||||
console.log('upload response',response)
|
console.log('upload response',response)
|
||||||
|
|||||||
@ -40,7 +40,6 @@ import { selectHospitalLoading, selectHospitals } from '../redux/hospitalSelecto
|
|||||||
import { SignUpData, SignUpStep } from '../types/signup';
|
import { SignUpData, SignUpStep } from '../types/signup';
|
||||||
import { authAPI } from '../services/authAPI';
|
import { authAPI } from '../services/authAPI';
|
||||||
import { showError, showSuccess } from '../../../shared/utils/toast';
|
import { showError, showSuccess } from '../../../shared/utils/toast';
|
||||||
import { createFormDataWithFile, validateFileType, validateFileSize } from '../../../shared/utils/fileUpload';
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// INTERFACES
|
// INTERFACES
|
||||||
@ -226,45 +225,26 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ navigation }) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let role = 'radiologist';
|
const formData = new FormData();
|
||||||
|
let role = 'er_physician';
|
||||||
|
|
||||||
// Prepare form data with proper file handling
|
formData.append('email', payload.email);
|
||||||
const formFields = {
|
formData.append('password', payload.password);
|
||||||
email: payload.email,
|
formData.append('first_name', payload.first_name);
|
||||||
password: payload.password,
|
formData.append('last_name', payload.last_name);
|
||||||
first_name: payload.first_name,
|
formData.append('username', payload.username);
|
||||||
last_name: payload.last_name,
|
formData.append('dashboard_role', role);
|
||||||
username: payload.username,
|
formData.append('hospital_id', payload.hospital_id);
|
||||||
dashboard_role: role,
|
|
||||||
hospital_id: payload.hospital_id,
|
// Attach file if exists
|
||||||
};
|
|
||||||
|
|
||||||
let formData: FormData;
|
|
||||||
|
|
||||||
// Handle file upload with validation
|
|
||||||
if (payload.id_photo_url) {
|
if (payload.id_photo_url) {
|
||||||
const fileData = {
|
const filePath = payload.id_photo_url;
|
||||||
uri: payload.id_photo_url,
|
const file = {
|
||||||
name: `id_photo_${Date.now()}.jpg`,
|
uri: filePath,
|
||||||
type: 'image/jpeg',
|
name: 'id_photo',
|
||||||
|
type: 'image/jpg',
|
||||||
};
|
};
|
||||||
|
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);
|
console.log('payload prepared', formData);
|
||||||
|
|||||||
@ -21,11 +21,7 @@ export const authAPI = {
|
|||||||
//fetch hospital list
|
//fetch hospital list
|
||||||
gethospitals: () => api.get('/api/hospitals/hospitals/app_user/hospitals', {},buildHeaders()),
|
gethospitals: () => api.get('/api/hospitals/hospitals/app_user/hospitals', {},buildHeaders()),
|
||||||
//user signup
|
//user signup
|
||||||
signup: (formData:any) => api.post('/api/auth/auth/admin/create-user-fromapp', formData, {
|
signup: (formData:any) => api.post('/api/auth/auth/admin/create-user-fromapp', formData,buildHeaders({ contentType: 'multipart/form-data' })),
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
//validate email
|
//validate email
|
||||||
validatemail: (payload:{email:string}) => api.post('/api/auth/auth/check-email', payload,buildHeaders()),
|
validatemail: (payload:{email:string}) => api.post('/api/auth/auth/check-email', payload,buildHeaders()),
|
||||||
//change password
|
//change password
|
||||||
@ -33,32 +29,7 @@ export const authAPI = {
|
|||||||
//validate username
|
//validate username
|
||||||
validateusername: (username:string|undefined) => api.post('/api/auth/auth/check-username', {username},buildHeaders()),
|
validateusername: (username:string|undefined) => api.post('/api/auth/auth/check-username', {username},buildHeaders()),
|
||||||
//upload document for onboarding
|
//upload document for onboarding
|
||||||
uploadDocument: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-id-photo', formData, {
|
uploadDocument: (formData:any, token:string | undefined) => api.post('/api/auth/onboarding/upload-id-photo', formData,buildHeaders({token:token, contentType: 'multipart/form-data' }))
|
||||||
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
|
// Add more endpoints as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -365,7 +365,7 @@ const styles = StyleSheet.create({
|
|||||||
|
|
||||||
// Header section
|
// Header section
|
||||||
header: {
|
header: {
|
||||||
// marginBottom: theme.spacing.lg,
|
marginBottom: theme.spacing.lg,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Main title
|
// Main title
|
||||||
@ -465,8 +465,10 @@ const styles = StyleSheet.create({
|
|||||||
// Pie chart container
|
// Pie chart container
|
||||||
pieChartContainer: {
|
pieChartContainer: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
marginBottom: theme.spacing.lg,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
borderRadius: theme.borderRadius.medium,
|
borderRadius: theme.borderRadius.medium,
|
||||||
|
padding: theme.spacing.md,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Legend container
|
// Legend container
|
||||||
@ -476,6 +478,7 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: theme.colors.backgroundAlt,
|
backgroundColor: theme.colors.backgroundAlt,
|
||||||
borderRadius: theme.borderRadius.medium,
|
borderRadius: theme.borderRadius.medium,
|
||||||
padding: theme.spacing.md,
|
padding: theme.spacing.md,
|
||||||
|
marginTop: theme.spacing.md,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Legend title
|
// Legend title
|
||||||
|
|||||||
@ -29,6 +29,33 @@ export const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
|||||||
{dashboard.shiftInfo.currentShift} Shift • {dashboard.shiftInfo.attendingPhysician}
|
{dashboard.shiftInfo.currentShift} Shift • {dashboard.shiftInfo.attendingPhysician}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -41,6 +68,9 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: theme.spacing.lg,
|
marginBottom: theme.spacing.lg,
|
||||||
...theme.shadows.medium,
|
...theme.shadows.medium,
|
||||||
},
|
},
|
||||||
|
header: {
|
||||||
|
marginBottom: theme.spacing.lg,
|
||||||
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: theme.typography.fontSize.displayMedium,
|
fontSize: theme.typography.fontSize.displayMedium,
|
||||||
fontFamily: theme.typography.fontFamily.bold,
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
@ -51,7 +81,37 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: theme.typography.fontSize.bodyMedium,
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
color: theme.colors.textSecondary,
|
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
|
* End of File: DashboardHeader.tsx
|
||||||
|
|||||||
@ -1,337 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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,5 +3,4 @@ export { CriticalAlerts } from './CriticalAlerts';
|
|||||||
export { DashboardHeader } from './DashboardHeader';
|
export { DashboardHeader } from './DashboardHeader';
|
||||||
export { QuickActions } from './QuickActions';
|
export { QuickActions } from './QuickActions';
|
||||||
export { DepartmentStats } from './DepartmentStats';
|
export { DepartmentStats } from './DepartmentStats';
|
||||||
export { BrainPredictionsOverview } from './BrainPredictionsOverview';
|
export { BrainPredictionsOverview } from './BrainPredictionsOverview';
|
||||||
export { FeedbackAnalysisPieChart } from './FeedbackAnalysisPieChart';
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.
|
|
||||||
*/
|
|
||||||
@ -1,101 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 screens
|
||||||
export { default as DashboardScreen } from './screens/DashboardScreen';
|
export { default as ERDashboardScreen } from './screens/ERDashboardScreen';
|
||||||
|
|
||||||
// Export navigation
|
// Export navigation
|
||||||
export {
|
export {
|
||||||
@ -14,7 +14,7 @@ export {
|
|||||||
DashboardStackParamList,
|
DashboardStackParamList,
|
||||||
DashboardNavigationProp,
|
DashboardNavigationProp,
|
||||||
DashboardScreenProps,
|
DashboardScreenProps,
|
||||||
DashboardScreenProps,
|
ERDashboardScreenProps,
|
||||||
PatientDetailsScreenProps,
|
PatientDetailsScreenProps,
|
||||||
AlertDetailsScreenProps,
|
AlertDetailsScreenProps,
|
||||||
DepartmentStatsScreenProps,
|
DepartmentStatsScreenProps,
|
||||||
@ -39,9 +39,6 @@ export { default as DashboardHeader } from './components/DashboardHeader';
|
|||||||
export { default as QuickActions } from './components/QuickActions';
|
export { default as QuickActions } from './components/QuickActions';
|
||||||
export { default as DepartmentStats } from './components/DepartmentStats';
|
export { default as DepartmentStats } from './components/DepartmentStats';
|
||||||
|
|
||||||
// Export hooks
|
|
||||||
export * from './hooks';
|
|
||||||
|
|
||||||
// Export Redux
|
// Export Redux
|
||||||
export {
|
export {
|
||||||
fetchDashboardData,
|
fetchDashboardData,
|
||||||
@ -54,36 +51,6 @@ export {
|
|||||||
updateDashboardData,
|
updateDashboardData,
|
||||||
} from './redux/dashboardSlice';
|
} 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 {
|
export {
|
||||||
fetchAlerts,
|
fetchAlerts,
|
||||||
acknowledgeAlert,
|
acknowledgeAlert,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import React from 'react';
|
|||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
|
||||||
// Import dashboard screens
|
// Import dashboard screens
|
||||||
import { DashboardScreen } from '../screens/DashboardScreen';
|
import { ERDashboardScreen } from '../screens/ERDashboardScreen';
|
||||||
|
|
||||||
// Import navigation types
|
// Import navigation types
|
||||||
import { DashboardStackParamList } from './navigationTypes';
|
import { DashboardStackParamList } from './navigationTypes';
|
||||||
@ -22,7 +22,7 @@ const Stack = createStackNavigator<DashboardStackParamList>();
|
|||||||
* DashboardStackNavigator - Manages navigation between dashboard screens
|
* DashboardStackNavigator - Manages navigation between dashboard screens
|
||||||
*
|
*
|
||||||
* This navigator handles the flow between:
|
* This navigator handles the flow between:
|
||||||
* - DashboardScreen: Main ER dashboard with patient overview
|
* - ERDashboardScreen: Main ER dashboard with patient overview
|
||||||
* - Future screens: Patient details, alerts, reports, etc.
|
* - Future screens: Patient details, alerts, reports, etc.
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
@ -72,7 +72,7 @@ const DashboardStackNavigator: React.FC = () => {
|
|||||||
{/* ER Dashboard Screen - Main dashboard entry point */}
|
{/* ER Dashboard Screen - Main dashboard entry point */}
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ERDashboard"
|
name="ERDashboard"
|
||||||
component={DashboardScreen}
|
component={ERDashboardScreen}
|
||||||
options={{
|
options={{
|
||||||
title: 'ER Dashboard',
|
title: 'ER Dashboard',
|
||||||
headerShown: false, // Hide header for main dashboard
|
headerShown: false, // Hide header for main dashboard
|
||||||
|
|||||||
@ -13,12 +13,12 @@ export type {
|
|||||||
DashboardStackParamList,
|
DashboardStackParamList,
|
||||||
DashboardNavigationProp,
|
DashboardNavigationProp,
|
||||||
DashboardScreenProps,
|
DashboardScreenProps,
|
||||||
DashboardScreenProps,
|
ERDashboardScreenProps,
|
||||||
PatientDetailsScreenProps,
|
PatientDetailsScreenProps,
|
||||||
AlertDetailsScreenProps,
|
AlertDetailsScreenProps,
|
||||||
DepartmentStatsScreenProps,
|
DepartmentStatsScreenProps,
|
||||||
QuickActionsScreenProps,
|
QuickActionsScreenProps,
|
||||||
DashboardScreenParams,
|
ERDashboardScreenParams,
|
||||||
PatientDetailsScreenParams,
|
PatientDetailsScreenParams,
|
||||||
AlertDetailsScreenParams,
|
AlertDetailsScreenParams,
|
||||||
DepartmentStatsScreenParams,
|
DepartmentStatsScreenParams,
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { Patient, Alert as AlertType, ERDashboard } from '../../../shared/types'
|
|||||||
*/
|
*/
|
||||||
export type DashboardStackParamList = {
|
export type DashboardStackParamList = {
|
||||||
// ER Dashboard screen - Main dashboard with patient overview
|
// ER Dashboard screen - Main dashboard with patient overview
|
||||||
ERDashboard: DashboardScreenParams;
|
ERDashboard: ERDashboardScreenParams;
|
||||||
|
|
||||||
// Patient Details screen - Detailed patient information
|
// Patient Details screen - Detailed patient information
|
||||||
PatientDetails: PatientDetailsScreenParams;
|
PatientDetails: PatientDetailsScreenParams;
|
||||||
@ -59,7 +59,7 @@ export interface DashboardScreenProps<T extends keyof DashboardStackParamList> {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DashboardScreenParams
|
* ERDashboardScreenParams
|
||||||
*
|
*
|
||||||
* Purpose: Parameters passed to the ER dashboard screen
|
* 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
|
* - filter: Optional filter to apply to dashboard data
|
||||||
* - refresh: Optional flag to force refresh
|
* - refresh: Optional flag to force refresh
|
||||||
*/
|
*/
|
||||||
export interface DashboardScreenParams {
|
export interface ERDashboardScreenParams {
|
||||||
filter?: 'all' | 'critical' | 'active' | 'pending';
|
filter?: 'all' | 'critical' | 'active' | 'pending';
|
||||||
refresh?: boolean;
|
refresh?: boolean;
|
||||||
}
|
}
|
||||||
@ -140,9 +140,9 @@ export interface QuickActionsScreenParams {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DashboardScreenProps - Props for DashboardScreen component
|
* ERDashboardScreenProps - Props for ERDashboardScreen component
|
||||||
*/
|
*/
|
||||||
export type DashboardScreenProps = DashboardScreenProps<'ERDashboard'>;
|
export type ERDashboardScreenProps = DashboardScreenProps<'ERDashboard'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PatientDetailsScreenProps - Props for PatientDetailsScreen component
|
* PatientDetailsScreenProps - Props for PatientDetailsScreen component
|
||||||
|
|||||||