first commit

This commit is contained in:
yashwin-foxy 2025-08-20 20:42:33 +05:30
commit a09073a99d
237 changed files with 59874 additions and 0 deletions

2
.bundle/config Normal file
View File

@ -0,0 +1,2 @@
BUNDLE_PATH: "vendor/bundle"
BUNDLE_FORCE_RUBY_PLATFORM: 1

351
.cursor/rules/appflow.mdc Normal file
View File

@ -0,0 +1,351 @@
---
alwaysApply: true
---
# Physician App - Application Flow Rules
## 🚀 App Launch & Authentication Flow
### 1. Initial App Launch
```
App Launch → Splash Screen → Authentication Check
[Authenticated] → ER Dashboard
[Not Authenticated] → Login Screen
```
### 2. Authentication Flow Rules
```
Login Screen Options:
├── Hospital SSO Login (Primary)
├── Credential Login (Fallback)
├── Emergency Access (Quick)
└── Temporary Login (Limited)
```
### 3. SSO Integration Rules
- **Primary**: Hospital SSO integration
- **Fallback**: Username/password authentication
- **Emergency**: Quick access codes for urgent situations
- **Remember Device**: 30-day device authentication
- **Session Timeout**: 8 hours for security
## 📊 ER Dashboard Flow
### 4. Dashboard Entry Points
```
ER Dashboard → Load Patient List → Real-time Updates
├── Critical Alerts (Priority 1)
├── Pending Scans (Priority 2)
├── Recent Reports (Priority 3)
└── All Patients (Complete View)
```
### 5. Real-time Data Flow
```
WebSocket Connection → Live Updates
├── Patient Status Changes
├── New Scan Results
├── Critical Findings
├── Bed Assignments
└── Shift Changes
```
### 6. Alert Priority System
- **🔴 Critical**: Immediate action required (0-2 minutes)
- **🟡 Warning**: Attention needed (2-10 minutes)
- **🟢 Info**: Routine updates (10+ minutes)
- **🔵 Status**: General information
## 🚨 Critical Finding Response Workflow
### 7. Critical Alert Reception
```
Push Notification → Alert Screen → Patient Details
AI Summary → Image Review → Treatment Protocol
Consultation → Action → Documentation
```
### 8. Alert Response Timeline
- **0-30 seconds**: Alert received & acknowledged
- **30-60 seconds**: Patient details reviewed
- **1-2 minutes**: AI findings assessed
- **2-5 minutes**: Treatment decision made
- **5+ minutes**: Action initiated & documented
### 9. Critical Alert Components
```
Alert Screen Elements:
├── Patient Identification
├── Bed Location
├── Critical Finding Type
├── AI-Generated Summary
├── Urgency Level
├── Time Stamp
└── Action Buttons
```
## 🏥 Patient Care Flow
### 10. Patient Selection Flow
```
Patient List → Patient Card → Patient Details
├── Vital Signs
├── Medical History
├── Current Medications
├── Allergy Information
├── Bed Information
└── Admission Details
```
### 11. Patient Detail Navigation
```
Patient Details Screen:
├── Demographics (Top)
├── Vital Signs (Real-time)
├── Medical History (Expandable)
├── Current Medications (List)
├── Allergy Information (Alert)
├── Bed Information (Status)
└── Action Buttons (Bottom)
```
### 12. Medical Record Integration
- **EMR Sync**: Real-time patient data
- **Vital Signs**: Live monitoring integration
- **Medication History**: Pharmacy system sync
- **Lab Results**: Laboratory system integration
- **Imaging**: PACS system connection
## 📱 Screen Navigation Patterns
### 13. Primary Navigation Structure
```
Bottom Tab Navigation:
├── Dashboard (Home)
├── Patients
├── Alerts
├── Reports
└── Settings
```
### 14. Secondary Navigation
```
Stack Navigation per Tab:
├── List View → Detail View
├── Detail View → Action View
├── Action View → Confirmation
└── Confirmation → Return to List
```
### 15. Modal Navigation Rules
- **Quick Actions**: Modal overlays
- **Critical Actions**: Full-screen modals
- **Confirmation**: Alert modals
- **Settings**: Sheet modals
## 🔄 State Management Flow
### 16. Redux State Structure
```
Root State:
├── Auth (Authentication state)
├── Dashboard (ER dashboard data)
├── PatientCare (Patient information)
├── Alerts (Notification system)
├── Settings (User preferences)
└── UI (Interface state)
```
### 17. Data Flow Patterns
```
API Call → Redux Action → State Update → UI Re-render
WebSocket → Real-time Update → Immediate UI Change
User Action → Local State → API Call → Server Sync
```
### 18. Caching Strategy
- **Patient Data**: 15-minute cache
- **Critical Alerts**: No cache (real-time)
- **User Settings**: Persistent storage
- **Medical Records**: 5-minute cache
## 📋 Workflow Rules
### 19. Critical Finding Workflow
```
Step 1: Alert Reception
├── Push notification received
├── Alert screen displayed
├── Patient context loaded
└── AI summary generated
Step 2: Assessment
├── Patient details reviewed
├── Medical history checked
├── Current status assessed
└── Urgency level determined
Step 3: Action Planning
├── Treatment protocol loaded
├── Specialist consultation initiated
├── Emergency procedures prepared
└── Documentation started
Step 4: Implementation
├── Actions executed
├── Status updated
├── Team notified
└── Record documented
```
### 20. Routine Scan Processing
```
Step 1: Report Notification
├── Scan completion notification
├── Report status update
├── AI findings summary
└── Priority assignment
Step 2: Review Process
├── Report details loaded
├── Images reviewed
├── Findings assessed
└── Action plan created
Step 3: Documentation
├── Patient record updated
├── Treatment plan documented
├── Follow-up scheduled
└── Discharge planning initiated
```
## 🔐 Security & Access Control
### 21. Authentication Rules
- **Session Management**: 8-hour timeout
- **Auto-logout**: Inactivity after 30 minutes
- **Device Remembering**: 30-day trusted devices
- **Emergency Access**: Limited functionality
- **Audit Trail**: All actions logged
### 22. Permission Levels
```
User Roles:
├── ER Physician (Full Access)
├── Resident (Limited Access)
├── Medical Student (Read-only)
├── Emergency Access (Critical Only)
└── Temporary Access (Time-limited)
```
### 23. Data Access Rules
- **Patient Data**: Role-based access
- **Critical Alerts**: All ER staff
- **Medical Records**: Authorized personnel only
- **Settings**: User-specific
- **Audit Logs**: Admin only
## 📊 Performance & Optimization
### 24. Loading States
```
Loading Hierarchy:
├── Critical Alerts (Immediate)
├── Patient List (Fast)
├── Patient Details (Medium)
├── Medical History (Medium)
└── Full Reports (Slow)
```
### 25. Offline Capabilities
- **Critical Alerts**: Always available
- **Patient List**: Cached data
- **Recent Reports**: Offline access
- **Settings**: Local storage
- **Sync**: Automatic when online
### 26. Error Handling
```
Error Recovery:
├── Network Errors → Retry with backoff
├── Authentication Errors → Re-login
├── Data Errors → Fallback to cache
├── Critical Errors → Emergency mode
└── UI Errors → Graceful degradation
```
## 🎯 User Experience Rules
### 27. Interaction Patterns
- **Critical Actions**: Confirmation required
- **Quick Actions**: One-tap execution
- **Navigation**: Intuitive flow
- **Feedback**: Immediate response
- **Accessibility**: WCAG 2.1 compliance
### 28. Visual Hierarchy
```
Priority Order:
├── Critical Alerts (Red, Large)
├── Active Patients (Blue, Medium)
├── Pending Items (Yellow, Medium)
├── Completed Items (Green, Small)
└── Background Info (Gray, Small)
```
### 29. Responsive Design
- **Mobile First**: Optimized for phones
- **Tablet Support**: Enhanced layouts
- **Landscape Mode**: Alternative views
- **Accessibility**: Voice commands support
## 🔄 Data Synchronization
### 30. Real-time Updates
```
Update Types:
├── Patient Status (Immediate)
├── Vital Signs (30-second intervals)
├── Alert Status (Real-time)
├── Bed Assignments (Real-time)
└── Report Status (5-minute intervals)
```
### 31. Conflict Resolution
- **Server Priority**: Server data overrides local
- **Timestamp Comparison**: Latest data wins
- **User Confirmation**: Manual resolution for conflicts
- **Audit Trail**: All changes tracked
## 📱 Device Integration
### 32. Hardware Integration
- **Camera**: Document scanning
- **Microphone**: Voice notes
- **Biometrics**: Secure access
- **NFC**: Patient identification
- **Bluetooth**: Medical device connection
### 33. Platform-Specific Features
```
iOS Features:
├── Face ID authentication
├── Apple Health integration
├── Siri shortcuts
└── iOS notifications
Android Features:
├── Fingerprint authentication
├── Google Fit integration
├── Android Auto
└── Android notifications
```
This comprehensive flow ensures efficient, secure, and user-friendly operation of the Physician App in emergency medical scenarios.

View File

@ -0,0 +1,308 @@
---
alwaysApply: true
---
# Physician App - Project Structure & File Naming Rules
## 📁 Directory Structure Rules
### 1. Root Level Organization
```
NeoScan_Physician/
├── app/ # Main application code
├── docs/ # Documentation
├── android/ # Android native code
├── ios/ # iOS native code
├── index.js # React Native entry point
├── package.json # Dependencies
├── tsconfig.json # TypeScript config
├── metro.config.js # Metro bundler config
├── babel.config.js # Babel config
└── .eslintrc.js # ESLint config
```
### 2. App Directory Structure
```
app/
├── modules/ # Feature-based modules
├── shared/ # Shared utilities & components
├── store/ # Redux store configuration
├── navigation/ # Navigation setup
├── theme/ # Styling & theming
├── config/ # Configuration files
├── assets/ # Static assets
├── localization/ # i18n
├── App.tsx # Root component
└── index.tsx # App entry point
```
## 🏗️ Module Architecture Rules
### 3. Module Structure (Feature-based)
Each module MUST follow this structure:
```
modules/ModuleName/
├── components/ # Reusable UI components
├── screens/ # Screen components
├── hooks/ # Custom hooks
├── redux/ # State management
├── services/ # API & external services
├── __tests__/ # Test files
└── index.ts # Module exports
```
### 4. Required Modules
- **Auth/** - Authentication & SSO
- **Dashboard/** - ER Dashboard & patient tracking
- **PatientCare/** - Patient details & medical records
- **Settings/** - User preferences & app settings
## 📝 File Naming Conventions
### 5. Component Files
- **PascalCase** for all component files
- **Suffix with type**: `.tsx` for components, `.ts` for utilities
- **Examples**:
- `LoginScreen.tsx`
- `PatientCard.tsx`
- `CriticalAlerts.tsx`
- `HospitalSSO.tsx`
### 6. Hook Files
- **camelCase** with `use` prefix
- **Examples**:
- `useAuth.ts`
- `usePatientList.ts`
- `useRealTimeAlerts.ts`
- `useCriticalAlerts.ts`
### 7. Service Files
- **camelCase** with descriptive names
- **Suffix with type**: `API.ts`, `Service.ts`
- **Examples**:
- `authAPI.ts`
- `patientCareAPI.ts`
- `notificationService.ts`
- `emrIntegration.ts`
### 8. Redux Files
- **camelCase** with descriptive suffixes
- **Examples**:
- `authSlice.ts`
- `erDashboardSlice.ts`
- `patientCareActions.ts`
- `dashboardSelectors.ts`
### 9. Test Files
- **Same name as source file** + `.test.ts` or `.test.tsx`
- **Examples**:
- `LoginScreen.test.tsx`
- `useAuth.test.ts`
- `authSlice.test.ts`
- `PatientCard.test.tsx`
## 🔧 Shared Components Rules
### 10. UI Components Structure
```
shared/components/
├── UI/ # Basic UI components
│ ├── Button.tsx
│ ├── Input.tsx
│ ├── Card.tsx
│ ├── Modal.tsx
│ ├── Badge.tsx
│ ├── Spinner.tsx
│ ├── Alert.tsx
│ ├── Dropdown.tsx
│ ├── Tabs.tsx
│ ├── ProgressBar.tsx
│ └── index.ts
├── Forms/ # Form-related components
│ ├── FormField.tsx
│ ├── ValidationMessage.tsx
│ ├── FormContainer.tsx
│ └── index.ts
├── Icons/ # Icon components
│ ├── MedicalIcons.tsx
│ ├── StatusIcons.tsx
│ ├── NavigationIcons.tsx
│ └── index.ts
└── index.ts
```
### 11. Utility Files
```
shared/utils/
├── api.ts # API utilities
├── constants.ts # App constants
├── helpers.ts # Helper functions
├── validators.ts # Validation functions
├── formatters.ts # Data formatting
├── dateUtils.ts # Date utilities
├── medicalUtils.ts # Medical-specific utilities
├── imageUtils.ts # Image processing
├── stringUtils.ts # String manipulation
└── index.ts
```
## 🎨 Assets Organization
### 12. Image Assets
```
assets/images/
├── logos/ # Hospital & app logos
├── icons/ # UI icons
│ ├── medical/ # Medical-specific icons
│ ├── ui/ # General UI icons
│ └── status/ # Status indicators
├── backgrounds/ # Background images
└── placeholders/ # Placeholder images
```
### 13. Asset Naming
- **kebab-case** for all asset files
- **Examples**:
- `hospital-logo.png`
- `critical-alert.mp3`
- `ct-scan-placeholder.png`
- `emergency-bg.jpg`
## 📱 Navigation Structure
### 14. Navigation Files
```
navigation/
├── AppNavigator.tsx # Root navigator
├── AuthNavigator.tsx # Authentication flow
├── MainNavigator.tsx # Main app flow
├── TabNavigator.tsx # Tab navigation
├── navigationTypes.ts # Type definitions
├── navigationUtils.ts # Navigation utilities
└── __tests__/
```
## 🔐 Configuration Rules
### 15. Environment & Config
```
config/
├── env.ts # Environment variables
├── api.ts # API configuration
├── websocket.ts # WebSocket config
├── notifications.ts # Notification config
├── security.ts # Security settings
└── index.ts
```
## 📚 Documentation Rules
### 16. Documentation Structure
```
docs/
├── README.md # Project overview
├── ARCHITECTURE.md # Architecture documentation
├── API.md # API documentation
├── DEPLOYMENT.md # Deployment guide
├── TESTING.md # Testing guidelines
├── SECURITY.md # Security guidelines
└── wireframes/ # UI wireframes
```
## 🧪 Testing Rules
### 17. Test Organization
- **Unit tests** alongside source files in `__tests__/` folders
- **Integration tests** in module-level `__tests__/` folders
- **E2E tests** in root-level `__tests__/` folder
- **Test utilities** in `shared/__tests__/`
## 📦 Package Management
### 18. Dependencies Organization
- **Core dependencies** in root `package.json`
- **Platform-specific** dependencies in respective folders
- **Dev dependencies** clearly separated
- **Peer dependencies** explicitly declared
## 🔄 Import/Export Rules
### 19. Import Conventions
- **Absolute imports** for shared utilities
- **Relative imports** for module-internal files
- **Index files** for clean imports
- **Barrel exports** for module APIs
### 20. Export Patterns
```typescript
// Module index.ts
export { default as ComponentName } from './components/ComponentName';
export { useHookName } from './hooks/useHookName';
export { actionName } from './redux/actions';
export type { TypeName } from './types';
```
## 🚫 Naming Restrictions
### 21. Forbidden Patterns
- ❌ No spaces in file names
- ❌ No special characters except `-` and `_`
- ❌ No uppercase in utility/service files
- ❌ No generic names like `utils.ts` or `helpers.ts`
- ❌ No abbreviations unless universally understood
### 22. Required Patterns
- ✅ Descriptive, self-documenting names
- ✅ Consistent casing within categories
- ✅ Clear separation of concerns
- ✅ Meaningful directory structure
- ✅ Proper TypeScript extensions
## 📋 File Size Guidelines
### 23. Component Limits
- **Single component files**: Max 300 lines
- **Complex components**: Split into smaller components
- **Utility files**: Max 200 lines
- **Service files**: Max 150 lines per service
### 24. Module Limits
- **Module components**: Max 10 files per category
- **Module services**: Max 5 files
- **Module hooks**: Max 8 files
- **Module redux**: Max 6 files
## 🔍 Code Organization Principles
### 25. Separation of Concerns
- **UI Logic** in components
- **Business Logic** in hooks/services
- **State Management** in Redux
- **Data Fetching** in services
- **Utilities** in shared/utils
### 26. Reusability
- **Shared components** in shared/components
- **Common utilities** in shared/utils
- **Type definitions** in shared/types
- **Constants** in shared/constants
This structure ensures maintainability, scalability, and consistency across the Physician App codebase.
and it should add proper comments in the each file for better understanding the flow.
should follow rule file while generating code.Each file should contain this as header
/*
* File: FILE_NAME.tsx
* Description: Main chat screen component
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
file footer
/*
* End of File: ChatScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
and it should add proper comments in the file for better understanding the flow.
should follow rule file while generating code.

718
.cursor/rules/themeflow.mdc Normal file
View File

@ -0,0 +1,718 @@
---
alwaysApply: true
---
# Physician App - Theme & UI Design Rules
## 🎨 Color Palette - "Modern Healthcare Blue"
### 1. Primary Color Scheme
```typescript
// Primary Colors
Primary: '#2196F3' // Material Blue - Main brand color
Secondary: '#1976D2' // Darker Blue - Secondary actions
Tertiary: '#E3F2FD' // Very Light Blue - Backgrounds
Quaternary: '#0D47A1' // Deep Blue - Accents
// Text Colors
TextPrimary: '#212121' // Dark Gray - Main text
TextSecondary: '#757575' // Medium Gray - Secondary text
TextMuted: '#9E9E9E' // Light Gray - Muted text
// Background Colors
Background: '#FFFFFF' // White - Primary background
BackgroundAlt: '#FAFAFA' // Light Gray - Alternative background
BackgroundAccent: '#F5F5F5' // Soft Gray - Accent backgrounds
```
### 2. Status & Feedback Colors
```typescript
// Status Colors
Success: '#4CAF50' // Material Green - Success states
Warning: '#FF9800' // Material Orange - Warning states
Error: '#F44336' // Material Red - Error states
Info: '#2196F3' // Material Blue - Information states
// UI Elements
Border: '#E0E0E0' // Light Gray Border
CardBackground: '#FFFFFF' // White - Card backgrounds
Shadow: 'rgba(0, 0, 0, 0.1)' // Subtle Gray Shadow
```
## 🏗️ Typography System
### 3. Font Hierarchy
```typescript
// Font Families
PrimaryFont: 'Roboto' // Main font family
SecondaryFont: 'Medical-Icons' // Icon font
// Font Weights
Light: 300
Regular: 400
Medium: 500
Bold: 700
// Font Sizes
DisplayLarge: 32px // Main headings
DisplayMedium: 24px // Section headings
DisplaySmall: 20px // Subsection headings
BodyLarge: 16px // Body text
BodyMedium: 14px // Secondary text
BodySmall: 12px // Captions
Caption: 10px // Small labels
```
### 4. Line Heights & Spacing
```typescript
// Line Heights
Tight: 1.2 // Headings
Normal: 1.4 // Body text
Relaxed: 1.6 // Long text
// Letter Spacing
Tight: -0.5px // Headings
Normal: 0px // Body text
Wide: 0.5px // Labels
```
## 📐 Spacing & Layout
### 5. Spacing Scale
```typescript
// Base Spacing Unit: 4px
Spacing: {
xs: 4, // 4px
sm: 8, // 8px
md: 16, // 16px
lg: 24, // 24px
xl: 32, // 32px
xxl: 48, // 48px
xxxl: 64 // 64px
}
```
### 6. Layout Rules
```typescript
// Container Max Widths
Mobile: 375
Tablet: 768
Desktop: 1024
// Border Radius
Small: 4
Medium: 8
Large: 12
XLarge: 16
Round: 50
// Shadows (React Native StyleSheet)
Small: {
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2
}
Medium: {
shadowColor: '#000000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 4
}
Large: {
shadowColor: '#000000',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.2,
shadowRadius: 16,
elevation: 8
}
```
## 🎯 Component Design Rules
### 7. Button Design System
```typescript
// Button Variants
Primary: {
backgroundColor: '#2196F3',
borderColor: '#2196F3',
borderRadius: 8,
shadowColor: '#2196F3',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3
}
Secondary: {
backgroundColor: 'transparent',
borderColor: '#2196F3',
borderRadius: 8
}
Success: {
backgroundColor: '#4CAF50',
borderColor: '#4CAF50',
borderRadius: 8,
shadowColor: '#4CAF50',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3
}
Critical: {
backgroundColor: '#F44336',
borderColor: '#F44336',
borderRadius: 8,
shadowColor: '#F44336',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3
}
// Button Sizes
Small: { paddingHorizontal: 16, paddingVertical: 8, fontSize: 14, borderRadius: 6 }
Medium: { paddingHorizontal: 24, paddingVertical: 12, fontSize: 16, borderRadius: 8 }
Large: { paddingHorizontal: 32, paddingVertical: 16, fontSize: 18, borderRadius: 10 }
```
### 8. Card Design Rules
```typescript
// Card Variants
Default: {
backgroundColor: '#FFFFFF',
borderColor: '#E0E0E0',
borderRadius: 12,
padding: 16,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2
}
Elevated: {
backgroundColor: '#FFFFFF',
borderColor: '#E0E0E0',
borderRadius: 12,
padding: 16,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 4
}
Critical: {
backgroundColor: '#FFEBEE',
borderColor: '#F44336',
borderRadius: 12,
padding: 16,
shadowColor: '#F44336',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2
}
PatientCard: {
backgroundColor: '#FFFFFF',
borderColor: '#E0E0E0',
borderRadius: 16,
padding: 20,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
elevation: 2
}
```
### 9. Input Field Design
```typescript
// Input States
Default: {
borderColor: '#E0E0E0',
backgroundColor: '#FFFFFF',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 12
}
Focused: {
borderColor: '#2196F3',
backgroundColor: '#FFFFFF',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 12
}
Error: {
borderColor: '#F44336',
backgroundColor: '#FFEBEE',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 12
}
Disabled: {
borderColor: '#E0E0E0',
backgroundColor: '#F5F5F5',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 12
}
```
## 🚨 Alert & Status Design (Following Screenshot UI)
### 10. Alert Priority System
```typescript
// Critical Alerts
Critical: {
backgroundColor: '#FFEBEE',
borderColor: '#F44336',
borderRadius: 8,
padding: 16
}
// Warning Alerts
Warning: {
backgroundColor: '#FFF3E0',
borderColor: '#FF9800',
borderRadius: 8,
padding: 16
}
// Success Alerts
Success: {
backgroundColor: '#E8F5E8',
borderColor: '#4CAF50',
borderRadius: 8,
padding: 16
}
// Info Alerts
Info: {
backgroundColor: '#E3F2FD',
borderColor: '#2196F3',
borderRadius: 8,
padding: 16
}
// Patient Status
Active: {
backgroundColor: '#E3F2FD',
borderColor: '#2196F3',
borderRadius: 12,
paddingHorizontal: 8,
paddingVertical: 4
}
Pending: {
backgroundColor: '#FFF3E0',
borderColor: '#FF9800',
borderRadius: 12,
paddingHorizontal: 8,
paddingVertical: 4
}
Completed: {
backgroundColor: '#E8F5E8',
borderColor: '#4CAF50',
borderRadius: 12,
paddingHorizontal: 8,
paddingVertical: 4
}
```
### 11. Badge & Tag Design (Following Screenshot UI)
```typescript
// Status Badges
StatusBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
fontSize: 12,
fontWeight: '500'
}
// Priority Badges
PriorityBadge: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
fontSize: 12,
fontWeight: '600'
}
// Medical Badges
MedicalBadge: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 14,
fontSize: 11,
fontWeight: '500'
}
```
## 📱 Screen-Specific Design Rules
### 12. ER Dashboard Screen Design (Following Screenshot UI)
```typescript
// Header Section
Header: {
backgroundColor: '#FFFFFF',
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomColor: '#E0E0E0',
borderBottomWidth: 1
}
// Critical Alerts Section (Like promotional banner but for emergencies)
CriticalAlerts: {
backgroundColor: '#FFEBEE',
borderColor: '#F44336',
borderRadius: 16,
padding: 20,
marginHorizontal: 16,
marginVertical: 12,
shadowColor: '#F44336',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 4
}
// Patient List Section (Like specialist filter)
PatientList: {
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: '#FFFFFF'
}
// Quick Actions (Like service icons but for emergency actions)
QuickActions: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: '#F5F5F5',
justifyContent: 'center',
alignItems: 'center',
marginHorizontal: 8
}
// Patient Cards (Like doctor cards but for patients)
PatientCard: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
marginHorizontal: 8,
marginVertical: 4,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
elevation: 2,
minWidth: 200
}
```
### 13. Patient Details Screen Design (Following Screenshot UI)
```typescript
// Patient Header
PatientHeader: {
backgroundColor: '#FFFFFF',
padding: 20,
borderBottomColor: '#E0E0E0',
borderBottomWidth: 1
}
// Patient Info Card
PatientInfoCard: {
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 20,
margin: 16,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 8,
elevation: 4
}
// Vital Signs Section (Like contact options but for medical data)
VitalSigns: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingVertical: 16,
borderBottomColor: '#E0E0E0',
borderBottomWidth: 1
}
// Medical History Section (Like calendar section but for medical records)
MedicalHistory: {
backgroundColor: '#FFFFFF',
borderRadius: 12,
padding: 16,
margin: 16,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
elevation: 2
}
// Action Buttons (Like booking button but for medical actions)
ActionButton: {
backgroundColor: '#2196F3',
borderRadius: 12,
padding: 16,
margin: 16,
alignItems: 'center',
shadowColor: '#2196F3',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 6
}
```
### 14. Login Screen Design (Following Screenshot UI)
```typescript
// Login Container
LoginContainer: {
backgroundColor: '#F1FDFF',
padding: 24,
borderRadius: 12,
shadowColor: '#2196F3',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 4
}
// Hospital Logo
Logo: {
width: 120,
height: 120,
marginBottom: 32
}
// Form Fields
FormField: {
marginBottom: 16,
backgroundColor: '#FFFFFF',
borderColor: '#E0E0E0',
borderRadius: 8
}
// Login Button
LoginButton: {
backgroundColor: '#2196F3',
borderRadius: 8,
padding: 16,
fontSize: 16,
fontWeight: '600'
}
```
## 🎨 Icon Design System
### 15. Icon Specifications
```typescript
// Icon Sizes
IconSizes: {
xs: 12, // Extra small
sm: 16, // Small
md: 24, // Medium (default)
lg: 32, // Large
xl: 48 // Extra large
}
// Icon Colors
IconColors: {
Primary: '#2196F3',
Secondary: '#1976D2',
Success: '#4CAF50',
Warning: '#FF9800',
Error: '#F44336',
Info: '#2196F3',
Muted: '#9E9E9E'
}
// Medical Icons
MedicalIcons: {
Stethoscope: 'medical-stethoscope',
Heart: 'medical-heart',
Brain: 'medical-brain',
Emergency: 'medical-emergency',
Patient: 'medical-patient',
Bed: 'medical-bed',
Alert: 'medical-alert',
VitalSigns: 'medical-vital-signs',
Medication: 'medical-medication',
Lab: 'medical-lab',
Imaging: 'medical-imaging',
Surgery: 'medical-surgery'
}
```
## 📊 Data Visualization
### 16. Chart Colors
```typescript
// Chart Color Palette
ChartColors: {
Primary: '#2196F3',
Secondary: '#1976D2',
Tertiary: '#E3F2FD',
Quaternary: '#0D47A1',
Success: '#4CAF50',
Warning: '#FF9800',
Error: '#F44336'
}
// Vital Signs Charts
VitalSignsChart: {
backgroundColor: '#FFFFFF',
gridColor: '#E0E0E0',
lineColor: '#2196F3',
pointColor: '#1976D2'
}
// Patient Status Charts
PatientStatusChart: {
backgroundColor: '#FFFFFF',
gridColor: '#E0E0E0',
lineColor: '#4CAF50',
pointColor: '#2E7D32'
}
```
## 🌙 Dark Mode Support
### 17. Dark Mode Colors
```typescript
// Dark Mode Palette
DarkMode: {
Background: '#121212',
BackgroundAlt: '#1E1E1E',
BackgroundAccent: '#2D2D2D',
TextPrimary: '#FFFFFF',
TextSecondary: '#B0B0B0',
TextMuted: '#808080',
Border: '#404040',
CardBackground: '#1E1E1E',
Primary: '#2196F3',
Secondary: '#1976D2'
}
```
## 📱 Responsive Design
### 18. Breakpoint System
```typescript
// Breakpoints
Breakpoints: {
Mobile: 375,
Tablet: 768,
Desktop: 1024,
LargeDesktop: 1440
}
// Responsive Spacing
ResponsiveSpacing: {
Mobile: { padding: 16, margin: 8 },
Tablet: { padding: 24, margin: 16 },
Desktop: { padding: 32, margin: 24 }
}
```
## 🎯 Accessibility Rules
### 19. Accessibility Standards
```typescript
// Color Contrast
ContrastRatios: {
Normal: 4.5, // Minimum for normal text
Large: 3.0, // Minimum for large text
UI: 3.0 // Minimum for UI elements
}
// Touch Targets
TouchTargets: {
Minimum: 44, // Minimum touch target size
Preferred: 48 // Preferred touch target size
}
// Focus Indicators
FocusIndicator: {
Color: '#2196F3',
Width: 2,
Style: 'solid'
}
```
## 🔄 Animation & Transitions
### 20. Animation Rules
```typescript
// Animation Durations
Durations: {
Fast: 150, // Quick interactions
Normal: 300, // Standard transitions
Slow: 500 // Complex animations
}
// Easing Functions
Easing: {
Standard: 'cubic-bezier(0.4, 0.0, 0.2, 1)',
Deceleration: 'cubic-bezier(0.0, 0.0, 0.2, 1)',
Acceleration: 'cubic-bezier(0.4, 0.0, 1, 1)'
}
// Transition Types
Transitions: {
Fade: { opacity: [0, 1], duration: 300 },
Slide: { transform: [{ translateY: [20, 0] }], duration: 300 },
Scale: { transform: [{ scale: [0.95, 1] }], duration: 200 }
}
```
## 🎨 Design Tokens
### 21. Design Token Structure
```typescript
// Theme Object Structure
Theme: {
colors: ColorPalette,
typography: TypographySystem,
spacing: SpacingScale,
borderRadius: BorderRadiusScale,
shadows: ShadowSystem,
breakpoints: BreakpointSystem,
animations: AnimationSystem
}
// Usage Example
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
padding: theme.spacing.md,
borderRadius: theme.borderRadius.medium,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2
}
});
```
This comprehensive theme system ensures consistency, accessibility, and modern healthcare aesthetics across the Physician App, following the clean and professional design patterns shown in the reference screenshots while maintaining the ER workflow functionality.

2
.env Normal file
View File

@ -0,0 +1,2 @@
BASE_URL='https://neoscan-backend.tech4bizsolutions.com'
# BASE_URL='http://192.168.1.87:3000'

4
.eslintrc.js Normal file
View File

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: '@react-native',
};

75
.gitignore vendored Normal file
View File

@ -0,0 +1,75 @@
# OSX
#
.DS_Store
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
**/.xcode.env.local
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
.cxx/
*.keystore
!debug.keystore
.kotlin/
# node.js
#
node_modules/
npm-debug.log
yarn-error.log
# fastlane
#
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/screenshots
**/fastlane/test_output
# Bundle artifact
*.jsbundle
# Ruby / CocoaPods
**/Pods/
/vendor/bundle/
# Temporary files created by Metro to check the health of the file watcher
.metro-health-check*
# testing
/coverage
# Yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

7
.prettierrc.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
arrowParens: 'avoid',
bracketSameLine: true,
bracketSpacing: false,
singleQuote: true,
trailingComma: 'all',
};

1
.watchmanconfig Normal file
View File

@ -0,0 +1 @@
{}

131
App.tsx Normal file
View File

@ -0,0 +1,131 @@
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
*/
import React from 'react';
import type {PropsWithChildren} from 'react';
import {
ScrollView,
StatusBar,
StyleSheet,
Text,
useColorScheme,
View,
} from 'react-native';
import {
Colors,
DebugInstructions,
Header,
LearnMoreLinks,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
type SectionProps = PropsWithChildren<{
title: string;
}>;
function Section({children, title}: SectionProps): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
return (
<View style={styles.sectionContainer}>
<Text
style={[
styles.sectionTitle,
{
color: isDarkMode ? Colors.white : Colors.black,
},
]}>
{title}
</Text>
<Text
style={[
styles.sectionDescription,
{
color: isDarkMode ? Colors.light : Colors.dark,
},
]}>
{children}
</Text>
</View>
);
}
function App(): React.JSX.Element {
const isDarkMode = useColorScheme() === 'dark';
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
/*
* To keep the template simple and small we're adding padding to prevent view
* from rendering under the System UI.
* For bigger apps the recommendation is to use `react-native-safe-area-context`:
* https://github.com/AppAndFlow/react-native-safe-area-context
*
* You can read more about it here:
* https://github.com/react-native-community/discussions-and-proposals/discussions/827
*/
const safePadding = '5%';
return (
<View style={backgroundStyle}>
<StatusBar
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
backgroundColor={backgroundStyle.backgroundColor}
/>
<ScrollView
style={backgroundStyle}>
<View style={{paddingRight: safePadding}}>
<Header/>
</View>
<View
style={{
backgroundColor: isDarkMode ? Colors.black : Colors.white,
paddingHorizontal: safePadding,
paddingBottom: safePadding,
}}>
<Section title="Step One">
Edit <Text style={styles.highlight}>App.tsx</Text> to change this
screen and then come back to see your edits.
</Section>
<Section title="See Your Changes">
<ReloadInstructions />
</Section>
<Section title="Debug">
<DebugInstructions />
</Section>
<Section title="Learn More">
Read the docs to discover what to do next:
</Section>
<LearnMoreLinks />
</View>
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
},
highlight: {
fontWeight: '700',
},
});
export default App;

16
Gemfile Normal file
View File

@ -0,0 +1,16 @@
source 'https://rubygems.org'
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'
# Ruby 3.4.0 has removed some libraries from the standard library.
gem 'bigdecimal'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'

355
PROJECT_STRUCTURE.md Normal file
View File

@ -0,0 +1,355 @@
/*
* File: PROJECT_STRUCTURE.md
* Description: Complete project structure and architecture documentation
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
# NeoScan Physician App - Project Structure
## 📁 Complete Directory Structure
```
NeoScan_Physician/
├── app/ # Main application code
│ ├── modules/ # Feature-based modules
│ │ ├── Auth/ # Authentication module
│ │ │ ├── components/ # Auth-specific components
│ │ │ ├── screens/ # Auth screens
│ │ │ │ └── LoginScreen.tsx # Main login screen
│ │ │ ├── hooks/ # Auth custom hooks
│ │ │ ├── redux/ # Auth state management
│ │ │ ├── services/ # Auth API services
│ │ │ └── index.ts # Auth module exports
│ │ ├── Dashboard/ # ER Dashboard module
│ │ │ ├── components/ # Dashboard components
│ │ │ │ ├── PatientCard.tsx # Patient information card
│ │ │ │ ├── CriticalAlerts.tsx # Critical alerts display
│ │ │ │ ├── DashboardHeader.tsx # Dashboard statistics header
│ │ │ │ ├── QuickActions.tsx # Emergency quick actions
│ │ │ │ └── DepartmentStats.tsx # Department statistics
│ │ │ ├── screens/ # Dashboard screens
│ │ │ │ └── DashboardScreen.tsx # Main ER dashboard
│ │ │ ├── hooks/ # Dashboard custom hooks
│ │ │ ├── redux/ # Dashboard state management
│ │ │ ├── services/ # Dashboard API services
│ │ │ └── index.ts # Dashboard module exports
│ │ ├── PatientCare/ # Patient management module
│ │ │ ├── components/ # Patient care components
│ │ │ ├── screens/ # Patient care screens
│ │ │ ├── hooks/ # Patient care hooks
│ │ │ ├── redux/ # Patient care state
│ │ │ ├── services/ # Patient care services
│ │ │ └── index.ts # Patient care exports
│ │ └── Settings/ # App settings module
│ │ ├── components/ # Settings components
│ │ ├── screens/ # Settings screens
│ │ ├── hooks/ # Settings hooks
│ │ ├── redux/ # Settings state
│ │ ├── services/ # Settings services
│ │ └── index.ts # Settings exports
│ ├── shared/ # Shared utilities & components
│ │ ├── components/ # Reusable UI components
│ │ │ ├── UI/ # Basic UI components
│ │ │ │ ├── Button.tsx # Button component
│ │ │ │ ├── Input.tsx # Input field component
│ │ │ │ ├── Card.tsx # Card component
│ │ │ │ ├── Modal.tsx # Modal component
│ │ │ │ ├── Badge.tsx # Badge component
│ │ │ │ ├── Spinner.tsx # Loading spinner
│ │ │ │ ├── Alert.tsx # Alert component
│ │ │ │ ├── Dropdown.tsx # Dropdown component
│ │ │ │ ├── Tabs.tsx # Tab component
│ │ │ │ ├── ProgressBar.tsx # Progress bar
│ │ │ │ └── index.ts # UI components export
│ │ │ ├── Forms/ # Form-related components
│ │ │ │ ├── FormField.tsx # Form field component
│ │ │ │ ├── ValidationMessage.tsx # Validation message
│ │ │ │ ├── FormContainer.tsx # Form container
│ │ │ │ └── index.ts # Form components export
│ │ │ ├── Icons/ # Icon components
│ │ │ │ ├── MedicalIcons.tsx # Medical-specific icons
│ │ │ │ ├── StatusIcons.tsx # Status indicators
│ │ │ │ ├── NavigationIcons.tsx # Navigation icons
│ │ │ │ └── index.ts # Icon components export
│ │ │ └── index.ts # Shared components export
│ │ ├── utils/ # Utility functions
│ │ │ ├── api.ts # API utilities
│ │ │ ├── constants.ts # App constants
│ │ │ ├── helpers.ts # Helper functions
│ │ │ ├── validators.ts # Validation functions
│ │ │ ├── formatters.ts # Data formatting
│ │ │ ├── dateUtils.ts # Date utilities
│ │ │ ├── medicalUtils.ts # Medical-specific utilities
│ │ │ ├── imageUtils.ts # Image processing
│ │ │ ├── stringUtils.ts # String manipulation
│ │ │ └── index.ts # Utils export
│ │ ├── types/ # TypeScript type definitions
│ │ │ ├── auth.ts # Authentication types
│ │ │ ├── patient.ts # Patient-related types
│ │ │ ├── dashboard.ts # Dashboard types
│ │ │ ├── alerts.ts # Alert types
│ │ │ ├── common.ts # Common types
│ │ │ └── index.ts # Types export
│ │ └── index.ts # Shared module export
│ ├── theme/ # Design system & theming
│ │ ├── colors.ts # Color palette
│ │ ├── typography.ts # Typography system
│ │ ├── spacing.ts # Spacing & layout
│ │ ├── shadows.ts # Shadow system
│ │ ├── animations.ts # Animation system
│ │ ├── theme.ts # Main theme object
│ │ └── index.ts # Theme export
│ ├── navigation/ # Navigation setup
│ │ ├── AppNavigator.tsx # Root navigator
│ │ ├── AuthNavigator.tsx # Authentication flow
│ │ ├── MainNavigator.tsx # Main app flow
│ │ ├── TabNavigator.tsx # Tab navigation
│ │ ├── navigationTypes.ts # Navigation types
│ │ ├── navigationUtils.ts # Navigation utilities
│ │ └── __tests__/ # Navigation tests
│ ├── store/ # Redux store configuration
│ │ ├── index.ts # Store configuration
│ │ ├── rootReducer.ts # Root reducer
│ │ ├── middleware.ts # Custom middleware
│ │ └── __tests__/ # Store tests
│ ├── config/ # Configuration files
│ │ ├── env.ts # Environment variables
│ │ ├── api.ts # API configuration
│ │ ├── websocket.ts # WebSocket config
│ │ ├── notifications.ts # Notification config
│ │ ├── security.ts # Security settings
│ │ └── index.ts # Config export
│ ├── assets/ # Static assets
│ │ ├── images/ # Image assets
│ │ │ ├── logos/ # Hospital & app logos
│ │ │ ├── icons/ # UI icons
│ │ │ │ ├── medical/ # Medical-specific icons
│ │ │ │ ├── ui/ # General UI icons
│ │ │ │ └── status/ # Status indicators
│ │ │ ├── backgrounds/ # Background images
│ │ │ └── placeholders/ # Placeholder images
│ │ ├── fonts/ # Font files
│ │ └── sounds/ # Audio assets
│ ├── localization/ # Internationalization
│ │ ├── en/ # English translations
│ │ ├── es/ # Spanish translations
│ │ ├── fr/ # French translations
│ │ └── index.ts # i18n configuration
│ ├── App.tsx # Root component
│ └── index.tsx # App entry point
├── android/ # Android native code
│ ├── app/ # Android app module
│ │ ├── build.gradle # App build configuration
│ │ ├── debug.keystore # Debug keystore
│ │ ├── proguard-rules.pro # ProGuard rules
│ │ └── src/ # Source code
│ │ ├── debug/ # Debug configuration
│ │ │ └── AndroidManifest.xml # Debug manifest
│ │ └── main/ # Main source
│ │ ├── AndroidManifest.xml # Main manifest
│ │ ├── java/ # Java source
│ │ │ └── com/ # Package structure
│ │ │ └── neoscan_physician/
│ │ │ ├── MainActivity.kt # Main activity
│ │ │ └── MainApplication.kt # Application class
│ │ └── res/ # Resources
│ │ ├── drawable/ # Drawable resources
│ │ ├── mipmap-*/ # App icons
│ │ └── values/ # Values
│ │ ├── strings.xml # String resources
│ │ └── styles.xml # Styles
│ ├── build.gradle # Project build configuration
│ ├── gradle/ # Gradle wrapper
│ ├── gradle.properties # Gradle properties
│ ├── gradlew # Gradle wrapper script
│ ├── gradlew.bat # Windows gradle wrapper
│ └── settings.gradle # Gradle settings
├── ios/ # iOS native code
│ ├── NeoScan_Physician/ # iOS app
│ │ ├── AppDelegate.swift # App delegate
│ │ ├── Images.xcassets/ # Image assets
│ │ │ ├── AppIcon.appiconset/ # App icons
│ │ │ └── Contents.json # Asset catalog
│ │ ├── Info.plist # App info
│ │ ├── LaunchScreen.storyboard # Launch screen
│ │ └── PrivacyInfo.xcprivacy # Privacy info
│ ├── NeoScan_Physician.xcodeproj/ # Xcode project
│ │ ├── project.pbxproj # Project file
│ │ └── xcshareddata/ # Shared data
│ │ └── xcschemes/ # Build schemes
│ │ └── NeoScan_Physician.xcscheme
│ └── Podfile # CocoaPods configuration
├── __tests__/ # Test files
│ ├── App.test.tsx # App component tests
│ ├── components/ # Component tests
│ ├── utils/ # Utility tests
│ └── integration/ # Integration tests
├── docs/ # Documentation
│ ├── README.md # Project overview
│ ├── ARCHITECTURE.md # Architecture documentation
│ ├── API.md # API documentation
│ ├── DEPLOYMENT.md # Deployment guide
│ ├── TESTING.md # Testing guidelines
│ ├── SECURITY.md # Security guidelines
│ └── wireframes/ # UI wireframes
├── .gitignore # Git ignore rules
├── .eslintrc.js # ESLint configuration
├── .prettierrc.js # Prettier configuration
├── .watchmanconfig # Watchman configuration
├── app.json # App configuration
├── babel.config.js # Babel configuration
├── jest.config.js # Jest configuration
├── metro.config.js # Metro bundler configuration
├── package.json # Dependencies and scripts
├── package-lock.json # Locked dependencies
├── tsconfig.json # TypeScript configuration
├── index.js # React Native entry point
├── setup.sh # Unix setup script
├── setup.bat # Windows setup script
├── README.md # Project README
├── PROJECT_STRUCTURE.md # This file
└── Gemfile # Ruby dependencies (iOS)
```
## 🏗️ Module Architecture
### Auth Module
**Purpose**: Handles all authentication and authorization functionality
- **LoginScreen**: Hospital SSO, credential login, emergency access
- **Components**: Login forms, authentication modals
- **Services**: Authentication API, token management
- **Redux**: Auth state, user session management
### Dashboard Module
**Purpose**: Main ER dashboard with patient monitoring and alerts
- **DashboardScreen**: Main dashboard with patient list and statistics
- **PatientCard**: Individual patient information display
- **CriticalAlerts**: High-priority alert notifications
- **QuickActions**: Emergency procedure shortcuts
- **DepartmentStats**: Real-time department overview
### PatientCare Module
**Purpose**: Comprehensive patient management and medical records
- **PatientDetailsScreen**: Complete patient information
- **VitalSigns**: Real-time vital signs monitoring
- **MedicalHistory**: Patient medical records
- **Medications**: Current medication management
- **Allergies**: Allergy information and alerts
### Settings Module
**Purpose**: App configuration and user preferences
- **SettingsScreen**: User preferences and app settings
- **NotificationSettings**: Alert and notification configuration
- **SecuritySettings**: Authentication and security options
- **AboutScreen**: App information and version details
## 📁 File Naming Conventions
### Components
- **PascalCase** for all component files
- **Suffix with type**: `.tsx` for components, `.ts` for utilities
- **Examples**: `LoginScreen.tsx`, `PatientCard.tsx`, `CriticalAlerts.tsx`
### Hooks
- **camelCase** with `use` prefix
- **Examples**: `useAuth.ts`, `usePatientList.ts`, `useRealTimeAlerts.ts`
### Services
- **camelCase** with descriptive names
- **Suffix with type**: `API.ts`, `Service.ts`
- **Examples**: `authAPI.ts`, `patientCareAPI.ts`, `notificationService.ts`
### Redux
- **camelCase** with descriptive suffixes
- **Examples**: `authSlice.ts`, `erDashboardSlice.ts`, `patientCareActions.ts`
### Tests
- **Same name as source file** + `.test.ts` or `.test.tsx`
- **Examples**: `LoginScreen.test.tsx`, `useAuth.test.ts`, `authSlice.test.ts`
## 🔧 Configuration Files
### Theme System
- **colors.ts**: Complete color palette with healthcare blue theme
- **typography.ts**: Font families, weights, sizes, and spacing
- **spacing.ts**: Spacing scale, border radius, breakpoints
- **shadows.ts**: Shadow system for elevation
- **animations.ts**: Animation durations and easing functions
- **theme.ts**: Main theme object combining all design tokens
### Type Definitions
- **auth.ts**: Authentication types and interfaces
- **patient.ts**: Patient data and medical record types
- **dashboard.ts**: Dashboard and ER management types
- **alerts.ts**: Alert and notification types
- **common.ts**: Common utility types and interfaces
### Utilities
- **constants.ts**: App constants, API configs, timeouts
- **helpers.ts**: Common utility functions and helpers
- **validators.ts**: Form validation and data validation
- **formatters.ts**: Data formatting and display utilities
- **medicalUtils.ts**: Medical-specific utility functions
## 🎨 Design System
### Color Palette
- **Primary**: #2196F3 (Material Blue)
- **Secondary**: #1976D2 (Darker Blue)
- **Critical**: #F44336 (Material Red)
- **Warning**: #FF9800 (Material Orange)
- **Success**: #4CAF50 (Material Green)
### Component Library
- **UI Components**: Basic building blocks (Button, Input, Card, etc.)
- **Form Components**: Form-specific components with validation
- **Icon Components**: Medical and UI icons
- **Layout Components**: Container and layout components
### Responsive Design
- **Mobile First**: Optimized for mobile devices
- **Tablet Support**: Enhanced layouts for tablets
- **Breakpoints**: Mobile (375px), Tablet (768px), Desktop (1024px)
## 🚀 Getting Started
1. **Clone the repository**
2. **Run setup script**: `./setup.sh` (Unix) or `setup.bat` (Windows)
3. **Start development server**: `npm start`
4. **Run on device**: `npm run android` or `npm run ios`
## 📱 Key Features Implemented
### ✅ Completed
- **Theme System**: Complete design system with healthcare blue theme
- **Authentication**: Login screen with SSO and credential options
- **ER Dashboard**: Main dashboard with patient cards and statistics
- **Patient Cards**: Comprehensive patient information display
- **Critical Alerts**: High-priority alert system
- **Quick Actions**: Emergency procedure shortcuts
- **Department Stats**: Real-time department overview
- **Navigation**: Tab-based navigation structure
- **Type Safety**: Complete TypeScript implementation
### 🔄 In Progress
- **Redux Store**: State management implementation
- **API Integration**: Backend service integration
- **Real-time Updates**: WebSocket implementation
- **Push Notifications**: Critical alert notifications
### 📋 Planned
- **Patient Details**: Comprehensive patient management screens
- **Medical Records**: Complete medical history management
- **Settings**: App configuration and preferences
- **Offline Support**: Offline data access and sync
- **Testing**: Comprehensive test suite
- **Documentation**: Complete API and user documentation
This structure provides a solid foundation for a comprehensive healthcare application with proper separation of concerns, type safety, and modern React Native best practices.
/*
* End of File: PROJECT_STRUCTURE.md
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

317
README.md Normal file
View File

@ -0,0 +1,317 @@
/*
* File: README.md
* Description: Project documentation and setup instructions
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
# NeoScan Physician App
A comprehensive React Native application designed for emergency department physicians to manage patient care, critical alerts, and medical workflows in real-time.
## 🚀 Features
### Authentication & Security
- **Hospital SSO Integration** - Seamless login with hospital credentials
- **Multi-factor Authentication** - Enhanced security for medical data
- **Emergency Access** - Quick access codes for urgent situations
- **Session Management** - 8-hour sessions with 30-minute inactivity timeout
- **Device Remembering** - 30-day trusted device authentication
### ER Dashboard
- **Real-time Patient Monitoring** - Live updates of patient status and vital signs
- **Critical Alert System** - Immediate notifications for life-threatening conditions
- **Department Statistics** - Overview of emergency, trauma, cardiac, neurology, pediatrics, and ICU
- **Quick Actions** - Emergency procedures, scan orders, medication, lab work, consultations
- **Patient Filtering** - Filter by status (All, Critical, Active)
### Patient Management
- **Comprehensive Patient Cards** - Vital signs, allergies, medications, diagnosis
- **Medical History** - Complete patient medical records
- **Real-time Vital Signs** - Blood pressure, heart rate, temperature, respiratory rate, oxygen saturation
- **Allergy Alerts** - Prominent display of patient allergies
- **Medication Tracking** - Current medications with dosages and schedules
### Critical Finding Response
- **AI-Powered Detection** - Automated critical finding identification
- **Immediate Alert System** - Push notifications for urgent cases
- **Response Timeline** - 0-30 seconds acknowledgment, 2-5 minutes action
- **Treatment Protocols** - Quick access to emergency procedures
## 🏗️ Architecture
### Project Structure
```
NeoScan_Physician/
├── app/ # Main application code
│ ├── modules/ # Feature-based modules
│ │ ├── Auth/ # Authentication module
│ │ ├── Dashboard/ # ER Dashboard module
│ │ ├── PatientCare/ # Patient management module
│ │ └── Settings/ # App settings module
│ ├── shared/ # Shared utilities & components
│ │ ├── components/ # Reusable UI components
│ │ ├── utils/ # Utility functions
│ │ └── types/ # TypeScript type definitions
│ ├── theme/ # Design system & theming
│ ├── navigation/ # Navigation setup
│ ├── store/ # Redux state management
│ ├── config/ # Configuration files
│ └── assets/ # Static assets
├── android/ # Android native code
├── ios/ # iOS native code
└── docs/ # Documentation
```
### Technology Stack
- **React Native 0.79.0** - Cross-platform mobile development
- **TypeScript** - Type-safe development
- **React Navigation 6** - Navigation management
- **Redux Toolkit** - State management
- **React Native Vector Icons** - Icon library
- **React Native Push Notification** - Real-time notifications
- **React Native Keychain** - Secure credential storage
## 🎨 Design System
### Color Palette - "Modern Healthcare Blue"
- **Primary**: #2196F3 (Material Blue)
- **Secondary**: #1976D2 (Darker Blue)
- **Critical**: #F44336 (Material Red)
- **Warning**: #FF9800 (Material Orange)
- **Success**: #4CAF50 (Material Green)
### Typography
- **Primary Font**: Roboto
- **Font Weights**: Light (300), Regular (400), Medium (500), Bold (700)
- **Font Sizes**: Display (32px, 24px, 20px), Body (16px, 14px, 12px), Caption (10px)
### Components
- **Patient Cards** - Comprehensive patient information display
- **Critical Alerts** - High-priority notification system
- **Quick Actions** - Emergency procedure shortcuts
- **Department Stats** - Real-time department overview
- **Dashboard Header** - ER statistics and shift information
## 📱 Screens
### Authentication Flow
1. **Splash Screen** - App initialization and authentication check
2. **Login Screen** - Hospital SSO, credential login, emergency access
3. **Main Dashboard** - ER overview with patient list and alerts
### Main Application
1. **ER Dashboard** - Real-time patient monitoring and critical alerts
2. **Patient Details** - Comprehensive patient information and medical history
3. **Alerts Center** - Critical finding notifications and response
4. **Reports** - Medical reports and scan results
5. **Settings** - User preferences and app configuration
## 🔧 Setup & Installation
### Prerequisites
- Node.js >= 18
- React Native CLI
- Android Studio (for Android development)
- Xcode (for iOS development)
### Installation Steps
1. **Clone the repository**
```bash
git clone <repository-url>
cd NeoScan_Physician
```
2. **Install dependencies**
```bash
npm install
```
3. **iOS Setup** (macOS only)
```bash
cd ios
pod install
cd ..
```
4. **Start the development server**
```bash
npm start
```
5. **Run on device/simulator**
```bash
# Android
npm run android
# iOS
npm run ios
```
## 🚨 Critical Alert Workflow
### Alert Reception (0-30 seconds)
- Push notification received
- Alert screen displayed
- Patient context loaded
- AI summary generated
### Assessment (30-60 seconds)
- Patient details reviewed
- Medical history checked
- Current status assessed
- Urgency level determined
### Action Planning (1-2 minutes)
- Treatment protocol loaded
- Specialist consultation initiated
- Emergency procedures prepared
- Documentation started
### Implementation (2-5 minutes)
- Actions executed
- Status updated
- Team notified
- Record documented
## 🔐 Security Features
### Authentication
- **Session Management**: 8-hour timeout
- **Auto-logout**: Inactivity after 30 minutes
- **Device Remembering**: 30-day trusted devices
- **Emergency Access**: Limited functionality
- **Audit Trail**: All actions logged
### Data Protection
- **Encryption**: End-to-end data encryption
- **HIPAA Compliance**: Healthcare data protection
- **Secure Storage**: Encrypted local storage
- **Network Security**: HTTPS/TLS communication
## 📊 Performance Optimization
### Loading States
- **Critical Alerts**: Immediate loading
- **Patient List**: Fast loading with caching
- **Patient Details**: Medium loading
- **Medical History**: Optimized loading
- **Full Reports**: Background loading
### Offline Capabilities
- **Critical Alerts**: Always available
- **Patient List**: Cached data access
- **Recent Reports**: Offline viewing
- **Settings**: Local storage
- **Sync**: Automatic when online
## 🧪 Testing
### Test Structure
- **Unit Tests**: Component and utility testing
- **Integration Tests**: Module interaction testing
- **E2E Tests**: Complete workflow testing
- **Performance Tests**: Load and stress testing
### Running Tests
```bash
# Unit tests
npm test
# E2E tests
npm run test:e2e
# Performance tests
npm run test:performance
```
## 📱 Platform Support
### iOS Features
- Face ID authentication
- Apple Health integration
- Siri shortcuts
- iOS notifications
### Android Features
- Fingerprint authentication
- Google Fit integration
- Android Auto
- Android notifications
## 🔄 Real-time Updates
### WebSocket Integration
- **Patient Status**: Real-time updates
- **Vital Signs**: 30-second intervals
- **Alert Status**: Immediate updates
- **Bed Assignments**: Real-time changes
- **Report Status**: 5-minute intervals
### Data Synchronization
- **Server Priority**: Server data overrides local
- **Timestamp Comparison**: Latest data wins
- **User Confirmation**: Manual resolution for conflicts
- **Audit Trail**: All changes tracked
## 📋 Development Guidelines
### Code Style
- **TypeScript**: Strict type checking
- **ESLint**: Code quality enforcement
- **Prettier**: Code formatting
- **Conventional Commits**: Git commit messages
### Component Guidelines
- **Single Responsibility**: One component, one purpose
- **Reusability**: Shared components in shared/
- **Type Safety**: Full TypeScript coverage
- **Accessibility**: WCAG 2.1 compliance
### Performance Guidelines
- **Lazy Loading**: Components loaded on demand
- **Memoization**: React.memo for expensive components
- **Image Optimization**: Compressed and cached images
- **Bundle Size**: Minimal dependencies
## 🚀 Deployment
### Build Configuration
- **Environment Variables**: Separate configs for dev/staging/prod
- **Code Signing**: Proper certificate management
- **Bundle Optimization**: Minified and optimized builds
- **Asset Management**: Optimized images and fonts
### Release Process
1. **Development**: Feature development and testing
2. **Staging**: Integration testing and QA
3. **Production**: Final testing and deployment
4. **Monitoring**: Performance and error tracking
## 📞 Support
### Documentation
- **API Documentation**: Complete API reference
- **User Guide**: End-user documentation
- **Developer Guide**: Technical documentation
- **Troubleshooting**: Common issues and solutions
### Contact
- **Technical Support**: dev-support@neoscan.com
- **Emergency Support**: emergency-support@neoscan.com
- **Feature Requests**: features@neoscan.com
## 📄 License
This project is proprietary software developed for healthcare institutions. All rights reserved.
---
**NeoScan Physician App** - Empowering emergency care with real-time intelligence and seamless workflows.
/*
* End of File: README.md
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

618
THEME_FLOW.md Normal file
View File

@ -0,0 +1,618 @@
# Physician App - Theme System & Design Flow
## 🎨 Design System Overview
### **Project Theme Structure**
```
app/theme/
├── theme.ts # Main theme configuration
├── colors.ts # Color palette definitions
├── typography.ts # Font families, sizes, weights
├── spacing.ts # Spacing scale and breakpoints
├── shadows.ts # Shadow and elevation system
└── animations.ts # Animation durations and easing
```
---
## 🎯 Color Palette - "Modern Healthcare Blue"
### **Primary Colors**
```typescript
Primary: '#2196F3' // Material Blue - Main brand color
Secondary: '#1976D2' // Darker Blue - Secondary actions
Tertiary: '#E3F2FD' // Very Light Blue - Backgrounds
Quaternary: '#0D47A1' // Deep Blue - Accents
```
### **Text Colors**
```typescript
TextPrimary: '#212121' // Dark Gray - Main text
TextSecondary: '#757575' // Medium Gray - Secondary text
TextMuted: '#9E9E9E' // Light Gray - Muted text
```
### **Background Colors**
```typescript
Background: '#FFFFFF' // White - Primary background
BackgroundAlt: '#FAFAFA' // Light Gray - Alternative background
BackgroundAccent: '#F5F5F5' // Soft Gray - Accent backgrounds
```
### **Status & Feedback Colors**
```typescript
Success: '#4CAF50' // Material Green - Success states
Warning: '#FF9800' // Material Orange - Warning states
Error: '#F44336' // Material Red - Error states
Info: '#2196F3' // Material Blue - Information states
Critical: '#F44336' // Critical alerts and emergencies
```
### **UI Elements**
```typescript
Border: '#E0E0E0' // Light Gray Border
CardBackground: '#FFFFFF' // White - Card backgrounds
Shadow: 'rgba(0, 0, 0, 0.1)' // Subtle Gray Shadow
```
---
## 📝 Typography System
### **Font Families (Roboto)**
```typescript
// Available Font Families
fontFamily: {
bold: 'Roboto-Bold',
medium: 'Roboto-Medium',
regular: 'Roboto-Regular',
light: 'Roboto-Light',
semibold: 'Roboto-SemiBold',
extrabold: 'Roboto-ExtraBold',
}
```
### **Font Weights**
```typescript
fontWeight: {
light: '300',
regular: '400',
medium: '500',
bold: '700',
}
```
### **Font Sizes**
```typescript
fontSize: {
displayLarge: 32, // Main headings
displayMedium: 24, // Section headings
displaySmall: 20, // Subsection headings
bodyLarge: 16, // Body text
bodyMedium: 14, // Secondary text
bodySmall: 12, // Captions
caption: 10, // Small labels
}
```
### **Line Heights & Spacing**
```typescript
lineHeight: {
tight: 1.2, // Headings
normal: 1.4, // Body text
relaxed: 1.6, // Long text
}
letterSpacing: {
tight: -0.5, // Headings
normal: 0, // Body text
wide: 0.5, // Labels
}
```
---
## 📐 Spacing & Layout
### **Spacing Scale (Base: 4px)**
```typescript
spacing: {
xs: 4, // 4px
sm: 8, // 8px
md: 16, // 16px
lg: 24, // 24px
xl: 32, // 32px
xxl: 48, // 48px
xxxl: 64 // 64px
}
```
### **Border Radius**
```typescript
borderRadius: {
small: 4,
medium: 8,
large: 12,
xlarge: 16,
round: 50,
}
```
### **Breakpoints**
```typescript
breakpoints: {
mobile: 375,
tablet: 768,
desktop: 1024,
largeDesktop: 1440,
}
```
---
## 🎨 Icon System
### **React Native Vector Icons**
#### **Available Icon Sets**
```typescript
// Primary Icon Sets
import Icon from 'react-native-vector-icons/Feather'; // Clean, minimal icons
import Icon from 'react-native-vector-icons/MaterialIcons'; // Material Design icons
import Icon from 'react-native-vector-icons/Ionicons'; // iOS-style icons
```
#### **Icon Usage Guidelines**
```typescript
// Standard Icon Implementation
<Icon
name="mail" // Icon name
size={20} // Size in pixels
color={theme.colors.textSecondary} // Color from theme
style={styles.icon} // Additional styling
/>
// Icon Sizes
IconSizes: {
xs: 12, // Extra small
sm: 16, // Small
md: 24, // Medium (default)
lg: 32, // Large
xl: 48 // Extra large
}
```
#### **Common Icon Names**
```typescript
// Navigation Icons
'home', 'menu', 'arrow-left', 'arrow-right', 'chevron-down', 'chevron-up'
// Action Icons
'plus', 'minus', 'edit', 'delete', 'save', 'cancel', 'check', 'close'
// Medical Icons
'heart', 'user', 'settings', 'bell', 'search', 'filter', 'calendar'
// Status Icons
'check-circle', 'alert-circle', 'info', 'warning', 'error'
```
---
## 🏗️ Component Design Rules
### **Button Design System**
```typescript
// Button Variants
Primary: {
backgroundColor: theme.colors.primary,
borderColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
shadowColor: theme.colors.primary,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3
}
Secondary: {
backgroundColor: 'transparent',
borderColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium
}
Success: {
backgroundColor: theme.colors.success,
borderColor: theme.colors.success,
borderRadius: theme.borderRadius.medium,
shadowColor: theme.colors.success,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3
}
Critical: {
backgroundColor: theme.colors.critical,
borderColor: theme.colors.critical,
borderRadius: theme.borderRadius.medium,
shadowColor: theme.colors.critical,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3
}
```
### **Input Field Design**
```typescript
// Input States
Default: {
borderColor: theme.colors.border,
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md
}
Focused: {
borderColor: theme.colors.primary,
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md
}
Error: {
borderColor: theme.colors.critical,
backgroundColor: theme.colors.criticalBackground,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md
}
```
### **Card Design Rules**
```typescript
// Card Variants
Default: {
backgroundColor: theme.colors.cardBackground,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.large,
padding: theme.spacing.lg,
shadowColor: theme.colors.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2
}
Elevated: {
backgroundColor: theme.colors.cardBackground,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.large,
padding: theme.spacing.lg,
shadowColor: theme.colors.shadow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 4
}
```
---
## 🚨 Alert & Status Design
### **Alert Priority System**
```typescript
// Critical Alerts
Critical: {
backgroundColor: theme.colors.criticalBackground,
borderColor: theme.colors.critical,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md
}
// Warning Alerts
Warning: {
backgroundColor: theme.colors.warningBackground,
borderColor: theme.colors.warning,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md
}
// Success Alerts
Success: {
backgroundColor: theme.colors.successBackground,
borderColor: theme.colors.success,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md
}
// Info Alerts
Info: {
backgroundColor: theme.colors.infoBackground,
borderColor: theme.colors.info,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md
}
```
### **Status Badges**
```typescript
// Status Badges
StatusBadge: {
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.round,
fontSize: theme.typography.fontSize.bodySmall,
fontWeight: theme.typography.fontWeight.medium
}
// Priority Badges
PriorityBadge: {
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
borderRadius: theme.borderRadius.round,
fontSize: theme.typography.fontSize.bodySmall,
fontWeight: theme.typography.fontWeight.bold
}
```
---
## 📱 Screen-Specific Design Rules
### **Login Screen Design**
```typescript
// Login Container
LoginContainer: {
backgroundColor: theme.colors.background,
padding: theme.spacing.lg,
borderRadius: theme.borderRadius.large,
shadowColor: theme.colors.shadow,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 4
}
// Input Container with Icons
InputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.backgroundAlt,
borderColor: theme.colors.border,
borderWidth: 1,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: 2,
marginBottom: theme.spacing.md
}
// Input Icon
InputIcon: {
marginRight: theme.spacing.sm,
color: theme.colors.textSecondary
}
```
### **Dashboard Screen Design**
```typescript
// Header Section
Header: {
backgroundColor: theme.colors.background,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
borderBottomColor: theme.colors.border,
borderBottomWidth: 1
}
// Critical Alerts Section
CriticalAlerts: {
backgroundColor: theme.colors.criticalBackground,
borderColor: theme.colors.critical,
borderRadius: theme.borderRadius.large,
padding: theme.spacing.lg,
marginHorizontal: theme.spacing.md,
marginVertical: theme.spacing.md,
shadowColor: theme.colors.critical,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.15,
shadowRadius: 8,
elevation: 4
}
```
---
## 🔄 Animation & Transitions
### **Animation Rules**
```typescript
// Animation Durations
Durations: {
Fast: 150, // Quick interactions
Normal: 300, // Standard transitions
Slow: 500 // Complex animations
}
// Easing Functions
Easing: {
Standard: 'cubic-bezier(0.4, 0.0, 0.2, 1)',
Deceleration: 'cubic-bezier(0.0, 0.0, 0.2, 1)',
Acceleration: 'cubic-bezier(0.4, 0.0, 1, 1)'
}
// Transition Types
Transitions: {
Fade: { opacity: [0, 1], duration: 300 },
Slide: { transform: [{ translateY: [20, 0] }], duration: 300 },
Scale: { transform: [{ scale: [0.95, 1] }], duration: 200 }
}
```
---
## 🎯 Usage Guidelines
### **Theme Import**
```typescript
// Import theme in components
import { theme } from '../theme/theme';
// Usage in StyleSheet
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
padding: theme.spacing.md,
borderRadius: theme.borderRadius.medium,
shadowColor: theme.colors.shadow,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 2
},
title: {
fontSize: theme.typography.fontSize.displayMedium,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
fontFamily: theme.typography.fontFamily.bold
}
});
```
### **Icon Implementation**
```typescript
// Import icon
import Icon from 'react-native-vector-icons/Feather';
// Usage in components
<Icon
name="mail"
size={20}
color={theme.colors.textSecondary}
style={styles.icon}
/>
```
### **Font Usage**
```typescript
// Typography with font families
const styles = StyleSheet.create({
heading: {
fontSize: theme.typography.fontSize.displayMedium,
fontWeight: theme.typography.fontWeight.bold,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
lineHeight: theme.typography.lineHeight.tight
},
body: {
fontSize: theme.typography.fontSize.bodyLarge,
fontWeight: theme.typography.fontWeight.regular,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
lineHeight: theme.typography.lineHeight.normal
}
});
```
---
## 🔧 Configuration Files
### **TypeScript Declaration**
```typescript
// app/types/react-native-vector-icons.d.ts
declare module 'react-native-vector-icons/Feather' {
import { Component } from 'react';
import { TextProps } from 'react-native';
interface IconProps extends TextProps {
name: string;
size?: number;
color?: string;
}
export default class Icon extends Component<IconProps> {}
}
```
### **Font Configuration**
```json
// react-native.config.js
module.exports = {
assets: ['./app/assets/fonts/'],
};
```
---
## 📋 Best Practices
### **Design Consistency**
- ✅ Always use theme colors instead of hardcoded values
- ✅ Use consistent spacing from the spacing scale
- ✅ Apply proper typography hierarchy
- ✅ Use appropriate icon sizes and colors
- ✅ Maintain consistent border radius values
### **Performance**
- ✅ Use StyleSheet.create for all styles
- ✅ Minimize inline styles
- ✅ Use theme constants for repeated values
- ✅ Optimize icon usage with proper sizing
### **Accessibility**
- ✅ Maintain proper color contrast ratios
- ✅ Use semantic color names
- ✅ Provide adequate touch targets (44px minimum)
- ✅ Support dynamic text sizing
### **Maintainability**
- ✅ Keep theme centralized and well-documented
- ✅ Use semantic naming for colors and spacing
- ✅ Document any custom theme extensions
- ✅ Version control theme changes
---
## 🚀 Quick Reference
### **Common Theme Values**
```typescript
// Colors
theme.colors.primary // #2196F3
theme.colors.textPrimary // #212121
theme.colors.background // #FFFFFF
theme.colors.critical // #F44336
// Spacing
theme.spacing.md // 16px
theme.spacing.lg // 24px
theme.spacing.xl // 32px
// Typography
theme.typography.fontSize.bodyLarge // 16px
theme.typography.fontWeight.bold // 700
theme.typography.fontFamily.bold // 'Roboto-Bold'
// Border Radius
theme.borderRadius.medium // 8px
theme.borderRadius.large // 12px
theme.borderRadius.round // 50px
```
### **Icon Quick Reference**
```typescript
// Common Icons
<Icon name="mail" size={20} color={theme.colors.textSecondary} />
<Icon name="lock" size={20} color={theme.colors.textSecondary} />
<Icon name="eye" size={22} color={theme.colors.textSecondary} />
<Icon name="eye-off" size={22} color={theme.colors.textSecondary} />
<Icon name="check" size={24} color={theme.colors.success} />
<Icon name="alert-circle" size={24} color={theme.colors.critical} />
```
This comprehensive theme system ensures consistency, accessibility, and maintainability across the Physician App while providing a modern healthcare-focused design experience.

13
__tests__/App.test.tsx Normal file
View File

@ -0,0 +1,13 @@
/**
* @format
*/
import React from 'react';
import ReactTestRenderer from 'react-test-renderer';
import App from '../App';
test('renders correctly', async () => {
await ReactTestRenderer.act(() => {
ReactTestRenderer.create(<App />);
});
});

130
android/app/build.gradle Normal file
View File

@ -0,0 +1,130 @@
apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
apply from: project(':react-native-config').projectDir.getPath() + "/dotenv.gradle"
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '../..'
// root = file("../../")
// The folder where the react-native NPM package is. Default is ../../node_modules/react-native
// reactNativeDir = file("../../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen
// codegenDir = file("../../node_modules/@react-native/codegen")
// The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js
// cliFile = file("../../node_modules/react-native/cli.js")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The command to run when bundling. By default is 'bundle'
// bundleCommand = "ram-bundle"
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
/* Autolinking */
autolinkLibrariesWithApp()
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = false
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def enableSeparateBuildPerCPUArchitecture = true
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace "com.neoscan_physician"
splits {
abi {
enable true
include 'armeabi-v7a', 'arm64-v8a', 'x86'
universalApk false
}
}
defaultConfig {
applicationId "com.neoscan_physician"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")

BIN
android/app/debug.keystore Normal file

Binary file not shown.

10
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,10 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:usesCleartextTraffic="true"
tools:targetApi="28"
tools:ignore="GoogleAppIndexingWarning"/>
</manifest>

View File

@ -0,0 +1,27 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:supportsRtl="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
package com.neoscan_physician
import com.facebook.react.ReactActivity
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
import com.facebook.react.defaults.DefaultReactActivityDelegate
class MainActivity : ReactActivity() {
/**
* Returns the name of the main component registered from JavaScript. This is used to schedule
* rendering of the component.
*/
override fun getMainComponentName(): String = "NeoScan_Physician"
/**
* Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate]
* which allows you to enable New Architecture with a single boolean flags [fabricEnabled]
*/
override fun createReactActivityDelegate(): ReactActivityDelegate =
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
}

View File

@ -0,0 +1,44 @@
package com.neoscan_physician
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
}
}
}

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material"
>
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

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

View File

@ -0,0 +1,9 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
</style>
</resources>

21
android/build.gradle Normal file
View File

@ -0,0 +1,21 @@
buildscript {
ext {
buildToolsVersion = "35.0.0"
minSdkVersion = 24
compileSdkVersion = 35
targetSdkVersion = 35
ndkVersion = "27.1.12297006"
kotlinVersion = "2.0.21"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle")
classpath("com.facebook.react:react-native-gradle-plugin")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin")
}
}
apply plugin: "com.facebook.react.rootproject"

39
android/gradle.properties Normal file
View File

@ -0,0 +1,39 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
android/gradlew vendored Normal file
View File

@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
android/gradlew.bat vendored Normal file
View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,37 @@
{
"migIndex": 1,
"data": [
{
"path": "app/assets/fonts/WorkSans-Bold.ttf",
"sha1": "ec84061651ead3c3c5cbb61c2d338aca0bacdc1e"
},
{
"path": "app/assets/fonts/WorkSans-ExtraBold.ttf",
"sha1": "0b371d1dbfbdd15db880bbd129b239530c71accb"
},
{
"path": "app/assets/fonts/WorkSans-ExtraLight.ttf",
"sha1": "74596e55487e2961b6c43993698d658e2ceee77b"
},
{
"path": "app/assets/fonts/WorkSans-Light.ttf",
"sha1": "293e11dae7e8b930bf5eea0b06ca979531f22189"
},
{
"path": "app/assets/fonts/WorkSans-Medium.ttf",
"sha1": "c281f8454dd193c2260e43ae2de171c5dd4086e4"
},
{
"path": "app/assets/fonts/WorkSans-Regular.ttf",
"sha1": "5e0183b29b57c54595c62ac6bc223b21f1434226"
},
{
"path": "app/assets/fonts/WorkSans-SemiBold.ttf",
"sha1": "64b8fe156fafce221a0f66504255257053fc6062"
},
{
"path": "app/assets/fonts/WorkSans-Thin.ttf",
"sha1": "a62251331038fdd079c47bc413a350efbf702db8"
}
]
}

6
android/settings.gradle Normal file
View File

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

4
app.json Normal file
View File

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

224
app/App.tsx Normal file
View File

@ -0,0 +1,224 @@
/*
* File: App.tsx
* Description: Main application component with navigation setup
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useRef } from 'react';
import { StatusBar } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer, NavigationContainerRef } from '@react-navigation/native';
import { theme } from './theme/theme';
import { RootStackNavigator, setNavigationRef } from './navigation';
import { StoreProvider } from './store/Provider';
import Toast from 'react-native-toast-message';
import { useAppSelector } from './store/hooks';
import { selectIsAuthenticated } from './modules/Auth/redux/authSelectors';
// ============================================================================
// MOCK DATA SECTION - For demonstration and development purposes
// ============================================================================
// Mock dashboard data representing the current state of the ER
const mockDashboard = {
totalPatients: 24, // Total number of patients in ER
criticalPatients: 3, // Number of patients requiring immediate attention
pendingScans: 8, // Number of scans waiting for review
recentReports: 12, // Number of reports generated recently
bedOccupancy: 85, // Percentage of beds currently occupied
departmentStats: {
emergency: 8, // Patients in emergency department
trauma: 4, // Patients in trauma department
cardiac: 3, // Patients in cardiac department
neurology: 2, // Patients in neurology department
pediatrics: 5, // Patients in pediatrics department
icu: 2, // Patients in ICU
},
shiftInfo: {
currentShift: 'DAY' as const, // Current shift (DAY/NIGHT)
startTime: new Date(), // Shift start time
endTime: new Date(), // Shift end time
attendingPhysician: 'Dr. Smith', // Lead physician on duty
residents: ['Dr. Johnson', 'Dr. Williams'], // Resident physicians
nurses: ['Nurse Brown', 'Nurse Davis'], // Nursing staff
},
lastUpdated: new Date(), // Last time dashboard was updated
};
// Mock patient data representing real patients in the ER
const mockPatients = [
{
id: '1', // Unique patient identifier
mrn: 'MRN001', // Medical Record Number
firstName: 'John',
lastName: 'Doe',
dateOfBirth: new Date('1985-03-15'),
gender: 'MALE' as const,
age: 38,
bedNumber: 'A1', // Assigned bed number
roomNumber: '101', // Room number
admissionDate: new Date('2024-01-15'),
status: 'ACTIVE' as const, // Current patient status
priority: 'CRITICAL' as const, // Priority level for treatment
department: 'Emergency',
attendingPhysician: 'Dr. Smith',
allergies: [
{
id: '1',
name: 'Penicillin',
severity: 'SEVERE' as const,
reaction: 'Anaphylaxis'
},
],
medications: [
{
id: '1',
name: 'Morphine',
dosage: '2mg',
frequency: 'Every 4 hours',
route: 'IV', // Administration route
startDate: new Date(),
status: 'ACTIVE' as const,
prescribedBy: 'Dr. Smith',
},
],
vitalSigns: {
bloodPressure: { systolic: 140, diastolic: 90, timestamp: new Date() },
heartRate: { value: 95, timestamp: new Date() },
temperature: { value: 37.2, timestamp: new Date() },
respiratoryRate: { value: 18, timestamp: new Date() },
oxygenSaturation: { value: 98, timestamp: new Date() },
},
medicalHistory: [],
currentDiagnosis: 'Chest pain, rule out MI', // Current medical diagnosis
lastUpdated: new Date(),
},
{
id: '2',
mrn: 'MRN002',
firstName: 'Jane',
lastName: 'Smith',
dateOfBirth: new Date('1990-07-22'),
gender: 'FEMALE' as const,
age: 33,
bedNumber: 'B2',
roomNumber: '102',
admissionDate: new Date('2024-01-15'),
status: 'ACTIVE' as const,
priority: 'HIGH' as const,
department: 'Trauma',
attendingPhysician: 'Dr. Johnson',
allergies: [],
medications: [],
vitalSigns: {
bloodPressure: { systolic: 120, diastolic: 80, timestamp: new Date() },
heartRate: { value: 88, timestamp: new Date() },
temperature: { value: 36.8, timestamp: new Date() },
respiratoryRate: { value: 16, timestamp: new Date() },
oxygenSaturation: { value: 99, timestamp: new Date() },
},
medicalHistory: [],
currentDiagnosis: 'Multiple trauma from MVA', // MVA = Motor Vehicle Accident
lastUpdated: new Date(),
},
];
// Mock alerts representing critical notifications that require immediate attention
const mockAlerts = [
{
id: '1',
type: 'CRITICAL_FINDING' as const, // Type of alert
priority: 'CRITICAL' as const, // Priority level
title: 'Critical Finding Detected',
message: 'AI has detected a potential brain bleed in CT scan. Immediate review required.',
patientId: '1', // Associated patient
patientName: 'John Doe',
bedNumber: 'A1',
timestamp: new Date(), // When alert was generated
isRead: false, // Whether alert has been read
isAcknowledged: false, // Whether alert has been acknowledged
actionRequired: true, // Whether action is required
},
];
/**
* AppContent Component (Inner Component)
*
* Purpose: Inner component that uses Redux hooks for authentication state
*
* Features:
* 1. Connect to Redux store for authentication state
* 2. Set up navigation container with global reference
* 3. Configure status bar appearance
* 4. Render the main app navigator based on authentication state
*
* Navigation Flow:
* 1. App starts Check Redux authentication status
* 2. If not authenticated Show LoginScreen
* 3. If authenticated Show MainTabNavigator (dashboard)
*/
function AppContent() {
// ============================================================================
// REDUX STATE
// ============================================================================
// Get authentication state from Redux
const isAuthenticated = useAppSelector(selectIsAuthenticated);
// Navigation reference for programmatic navigation
const navigationRef = useRef<NavigationContainerRef<any> | null>(null);
// ============================================================================
// EFFECTS
// ============================================================================
// Set up navigation reference for global access
React.useEffect(() => {
setNavigationRef(navigationRef);
}, []);
// ============================================================================
// RENDER SECTION
// ============================================================================
return (
<SafeAreaProvider>
<NavigationContainer ref={navigationRef}>
<StatusBar
barStyle="dark-content" // Dark text on light background
backgroundColor={theme.colors.background} // Status bar background color
/>
<RootStackNavigator isAuthenticated={isAuthenticated} />
<Toast
position='bottom'
bottomOffset={20}
/>
</NavigationContainer>
</SafeAreaProvider>
);
}
/**
* App Component (Root Component)
*
* Purpose: Root component that wraps the entire application with Redux Provider
*
* Features:
* 1. Provide Redux store context
* 2. Wrap the main app content
* 3. Enable Redux state management throughout the app
*/
export default function App() {
return (
<StoreProvider>
<AppContent />
</StoreProvider>
);
}
/*
* End of File: App.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 816 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -0,0 +1,249 @@
/*
* File: AIPredictionCard.test.tsx
* Description: Unit tests for AI Prediction Card component
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import AIPredictionCard from '../components/AIPredictionCard';
import type { AIPredictionCase } from '../types';
// ============================================================================
// MOCK DATA
// ============================================================================
const mockPredictionCase: AIPredictionCase = {
patid: 'test-patient-001',
hospital_id: 'hospital-123',
prediction: {
label: 'midline shift',
finding_type: 'pathology',
clinical_urgency: 'urgent',
confidence_score: 0.96,
finding_category: 'abnormal',
primary_severity: 'high',
anatomical_location: 'brain',
},
created_at: '2024-01-15T10:30:00Z',
updated_at: '2024-01-15T10:30:00Z',
review_status: 'pending',
priority: 'critical',
};
const mockProps = {
predictionCase: mockPredictionCase,
onPress: jest.fn(),
onReview: jest.fn(),
onToggleSelect: jest.fn(),
isSelected: false,
showReviewButton: true,
};
// ============================================================================
// UNIT TESTS
// ============================================================================
describe('AIPredictionCard', () => {
beforeEach(() => {
jest.clearAllMocks();
});
// ============================================================================
// RENDERING TESTS
// ============================================================================
describe('Rendering', () => {
it('should render correctly with required props', () => {
const { getByText } = render(
<AIPredictionCard
predictionCase={mockPredictionCase}
onPress={mockProps.onPress}
/>
);
expect(getByText('test-patient-001')).toBeTruthy();
expect(getByText('Midline Shift')).toBeTruthy();
expect(getByText('96%')).toBeTruthy();
expect(getByText('Urgent')).toBeTruthy();
});
it('should render review button when showReviewButton is true', () => {
const { getByText } = render(<AIPredictionCard {...mockProps} />);
expect(getByText('Review')).toBeTruthy();
});
it('should not render review button when showReviewButton is false', () => {
const { queryByText } = render(
<AIPredictionCard {...mockProps} showReviewButton={false} />
);
expect(queryByText('Review')).toBeNull();
});
it('should not render review button when status is not pending', () => {
const reviewedCase = {
...mockPredictionCase,
review_status: 'reviewed' as const,
};
const { queryByText } = render(
<AIPredictionCard
{...mockProps}
predictionCase={reviewedCase}
/>
);
expect(queryByText('Review')).toBeNull();
});
it('should render selection checkbox when onToggleSelect is provided', () => {
const { getByRole } = render(<AIPredictionCard {...mockProps} />);
expect(getByRole('checkbox')).toBeTruthy();
});
it('should show selected state correctly', () => {
const { getByRole } = render(
<AIPredictionCard {...mockProps} isSelected={true} />
);
const checkbox = getByRole('checkbox');
expect(checkbox.props.accessibilityState.checked).toBe(true);
});
});
// ============================================================================
// INTERACTION TESTS
// ============================================================================
describe('Interactions', () => {
it('should call onPress when card is pressed', () => {
const { getByRole } = render(<AIPredictionCard {...mockProps} />);
fireEvent.press(getByRole('button'));
expect(mockProps.onPress).toHaveBeenCalledWith(mockPredictionCase);
});
it('should call onReview when review button is pressed', () => {
const { getByText } = render(<AIPredictionCard {...mockProps} />);
fireEvent.press(getByText('Review'));
expect(mockProps.onReview).toHaveBeenCalledWith('test-patient-001');
});
it('should call onToggleSelect when checkbox is pressed', () => {
const { getByRole } = render(<AIPredictionCard {...mockProps} />);
fireEvent.press(getByRole('checkbox'));
expect(mockProps.onToggleSelect).toHaveBeenCalledWith('test-patient-001');
});
});
// ============================================================================
// DATA FORMATTING TESTS
// ============================================================================
describe('Data formatting', () => {
it('should format confidence score as percentage', () => {
const { getByText } = render(<AIPredictionCard {...mockProps} />);
expect(getByText('96%')).toBeTruthy();
});
it('should capitalize text correctly', () => {
const { getByText } = render(<AIPredictionCard {...mockProps} />);
expect(getByText('Midline Shift')).toBeTruthy();
expect(getByText('Pathology')).toBeTruthy();
expect(getByText('Abnormal')).toBeTruthy();
});
it('should handle missing anatomical location', () => {
const caseWithoutLocation = {
...mockPredictionCase,
prediction: {
...mockPredictionCase.prediction,
anatomical_location: 'not_applicable',
},
};
const { queryByText } = render(
<AIPredictionCard
{...mockProps}
predictionCase={caseWithoutLocation}
/>
);
// Should not render location when it's 'not_applicable'
expect(queryByText('Not Applicable')).toBeNull();
});
});
// ============================================================================
// ACCESSIBILITY TESTS
// ============================================================================
describe('Accessibility', () => {
it('should have proper accessibility labels', () => {
const { getByLabelText } = render(<AIPredictionCard {...mockProps} />);
expect(
getByLabelText('AI Prediction case for patient test-patient-001')
).toBeTruthy();
});
it('should have proper accessibility hints', () => {
const { getByRole } = render(<AIPredictionCard {...mockProps} />);
const cardButton = getByRole('button');
expect(cardButton.props.accessibilityHint).toBe(
'Tap to view detailed prediction information'
);
});
});
// ============================================================================
// EDGE CASES TESTS
// ============================================================================
describe('Edge cases', () => {
it('should handle missing dates gracefully', () => {
const caseWithoutDates = {
...mockPredictionCase,
created_at: undefined,
updated_at: undefined,
};
const { getByText } = render(
<AIPredictionCard
{...mockProps}
predictionCase={caseWithoutDates}
/>
);
expect(getByText('N/A')).toBeTruthy();
});
it('should handle emergency urgency with special styling', () => {
const emergencyCase = {
...mockPredictionCase,
prediction: {
...mockPredictionCase.prediction,
clinical_urgency: 'emergency' as const,
},
};
const { getByText } = render(
<AIPredictionCard
{...mockProps}
predictionCase={emergencyCase}
/>
);
expect(getByText('Emergency')).toBeTruthy();
});
});
});
/*
* End of File: AIPredictionCard.test.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,361 @@
/*
* File: aiPredictionAPI.test.ts
* Description: Unit tests for AI Prediction API service
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { aiPredictionAPI } from '../services/aiPredictionAPI';
// Mock apisauce
jest.mock('apisauce', () => ({
create: jest.fn(() => ({
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn(),
})),
}));
// Mock API utilities
jest.mock('../../../shared/utils', () => ({
API_CONFIG: {
BASE_URL: 'https://test-api.com',
},
buildHeaders: jest.fn((options = {}) => ({
headers: {
'Content-Type': 'application/json',
...(options.token && { Authorization: `Bearer ${options.token}` }),
},
})),
}));
// ============================================================================
// MOCK DATA
// ============================================================================
const mockToken = 'test-token-123';
const mockResponse = {
ok: true,
data: {
success: true,
data: [
{
patid: 'test-001',
hospital_id: 'hospital-001',
prediction: {
label: 'test finding',
finding_type: 'pathology',
clinical_urgency: 'urgent',
confidence_score: 0.95,
finding_category: 'abnormal',
primary_severity: 'high',
anatomical_location: 'brain',
},
},
],
},
};
// ============================================================================
// UNIT TESTS
// ============================================================================
describe('AI Prediction API', () => {
let mockApi: any;
beforeEach(() => {
// Reset mocks
jest.clearAllMocks();
// Get the mocked API instance
const { create } = require('apisauce');
mockApi = create();
});
// ============================================================================
// GET ALL PREDICTIONS TESTS
// ============================================================================
describe('getAllPredictions', () => {
it('should call GET endpoint with correct parameters', async () => {
mockApi.get.mockResolvedValue(mockResponse);
const params = {
page: 1,
limit: 20,
urgency: 'urgent',
search: 'test',
};
await aiPredictionAPI.getAllPredictions(mockToken, params);
expect(mockApi.get).toHaveBeenCalledWith(
'/api/ai-cases/all-prediction-results',
params,
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
it('should call GET endpoint without parameters', async () => {
mockApi.get.mockResolvedValue(mockResponse);
await aiPredictionAPI.getAllPredictions(mockToken);
expect(mockApi.get).toHaveBeenCalledWith(
'/api/ai-cases/all-prediction-results',
{},
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
});
// ============================================================================
// GET CASE DETAILS TESTS
// ============================================================================
describe('getCaseDetails', () => {
it('should call GET endpoint with correct case ID', async () => {
const caseId = 'test-case-001';
mockApi.get.mockResolvedValue(mockResponse);
await aiPredictionAPI.getCaseDetails(caseId, mockToken);
expect(mockApi.get).toHaveBeenCalledWith(
`/api/ai-cases/prediction-details/${caseId}`,
{},
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
});
// ============================================================================
// UPDATE CASE REVIEW TESTS
// ============================================================================
describe('updateCaseReview', () => {
it('should call PUT endpoint with correct data', async () => {
const caseId = 'test-case-001';
const reviewData = {
review_status: 'reviewed' as const,
reviewed_by: 'Dr. Test',
review_notes: 'Test notes',
priority: 'high' as const,
};
mockApi.put.mockResolvedValue(mockResponse);
await aiPredictionAPI.updateCaseReview(caseId, reviewData, mockToken);
expect(mockApi.put).toHaveBeenCalledWith(
`/api/ai-cases/review/${caseId}`,
reviewData,
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
});
// ============================================================================
// GET STATISTICS TESTS
// ============================================================================
describe('getPredictionStats', () => {
it('should call GET endpoint with time range parameter', async () => {
mockApi.get.mockResolvedValue(mockResponse);
await aiPredictionAPI.getPredictionStats(mockToken, 'week');
expect(mockApi.get).toHaveBeenCalledWith(
'/api/ai-cases/statistics',
{ timeRange: 'week' },
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
it('should call GET endpoint without time range parameter', async () => {
mockApi.get.mockResolvedValue(mockResponse);
await aiPredictionAPI.getPredictionStats(mockToken);
expect(mockApi.get).toHaveBeenCalledWith(
'/api/ai-cases/statistics',
{},
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
});
// ============================================================================
// SEARCH PREDICTIONS TESTS
// ============================================================================
describe('searchPredictions', () => {
it('should call GET endpoint with search query and filters', async () => {
const query = 'test search';
const filters = {
urgency: ['urgent', 'emergency'],
severity: ['high'],
dateRange: { start: '2024-01-01', end: '2024-01-31' },
};
mockApi.get.mockResolvedValue(mockResponse);
await aiPredictionAPI.searchPredictions(query, mockToken, filters);
expect(mockApi.get).toHaveBeenCalledWith(
'/api/ai-cases/search',
{
q: query,
filters: JSON.stringify(filters),
},
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
it('should call GET endpoint with only search query', async () => {
const query = 'test search';
mockApi.get.mockResolvedValue(mockResponse);
await aiPredictionAPI.searchPredictions(query, mockToken);
expect(mockApi.get).toHaveBeenCalledWith(
'/api/ai-cases/search',
{ q: query },
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
});
// ============================================================================
// BULK OPERATIONS TESTS
// ============================================================================
describe('bulkUpdateReviews', () => {
it('should call PUT endpoint with case IDs and review data', async () => {
const caseIds = ['case-001', 'case-002', 'case-003'];
const reviewData = {
review_status: 'reviewed' as const,
reviewed_by: 'Dr. Test',
review_notes: 'Bulk review',
};
mockApi.put.mockResolvedValue(mockResponse);
await aiPredictionAPI.bulkUpdateReviews(caseIds, reviewData, mockToken);
expect(mockApi.put).toHaveBeenCalledWith(
'/api/ai-cases/bulk-review',
{ caseIds, reviewData },
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
});
// ============================================================================
// SUBMIT FEEDBACK TESTS
// ============================================================================
describe('submitPredictionFeedback', () => {
it('should call POST endpoint with feedback data', async () => {
const caseId = 'test-case-001';
const feedbackData = {
accuracy_rating: 4 as const,
is_accurate: true,
physician_diagnosis: 'Confirmed midline shift',
feedback_notes: 'Accurate prediction',
improvement_suggestions: 'None',
};
mockApi.post.mockResolvedValue(mockResponse);
await aiPredictionAPI.submitPredictionFeedback(caseId, feedbackData, mockToken);
expect(mockApi.post).toHaveBeenCalledWith(
`/api/ai-cases/feedback/${caseId}`,
feedbackData,
expect.objectContaining({
headers: expect.objectContaining({
Authorization: `Bearer ${mockToken}`,
}),
})
);
});
});
// ============================================================================
// ERROR HANDLING TESTS
// ============================================================================
describe('Error handling', () => {
it('should handle API errors gracefully', async () => {
const errorResponse = {
ok: false,
problem: 'NETWORK_ERROR',
data: null,
};
mockApi.get.mockResolvedValue(errorResponse);
const result = await aiPredictionAPI.getAllPredictions(mockToken);
expect(result).toEqual(errorResponse);
});
it('should handle missing token', async () => {
mockApi.get.mockResolvedValue(mockResponse);
await aiPredictionAPI.getAllPredictions('');
expect(mockApi.get).toHaveBeenCalledWith(
'/api/ai-cases/all-prediction-results',
{},
expect.objectContaining({
headers: expect.objectContaining({
'Content-Type': 'application/json',
}),
})
);
});
});
});
/*
* End of File: aiPredictionAPI.test.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,231 @@
/*
* File: aiPredictionSlice.test.ts
* Description: Unit tests for AI Prediction Redux slice
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import aiPredictionReducer, {
setSearchQuery,
setUrgencyFilter,
setSeverityFilter,
setCategoryFilter,
clearAllFilters,
toggleShowFilters,
clearError,
} from '../redux/aiPredictionSlice';
import type { AIPredictionState } from '../types';
// ============================================================================
// MOCK DATA
// ============================================================================
const initialState: AIPredictionState = {
predictionCases: [],
currentCase: null,
isLoading: false,
isRefreshing: false,
isLoadingCaseDetails: false,
error: null,
searchQuery: '',
selectedUrgencyFilter: 'all',
selectedSeverityFilter: 'all',
selectedCategoryFilter: 'all',
sortBy: 'date',
sortOrder: 'desc',
currentPage: 1,
itemsPerPage: 20,
totalItems: 0,
lastUpdated: null,
cacheExpiry: null,
showFilters: false,
selectedCaseIds: [],
};
// ============================================================================
// UNIT TESTS
// ============================================================================
describe('AI Prediction Slice', () => {
// ============================================================================
// INITIAL STATE TESTS
// ============================================================================
it('should return the initial state', () => {
const result = aiPredictionReducer(undefined, { type: 'unknown' });
expect(result).toEqual(initialState);
});
// ============================================================================
// SEARCH TESTS
// ============================================================================
describe('Search functionality', () => {
it('should handle setSearchQuery', () => {
const searchQuery = 'test search';
const action = setSearchQuery(searchQuery);
const result = aiPredictionReducer(initialState, action);
expect(result.searchQuery).toBe(searchQuery);
expect(result.currentPage).toBe(1); // Should reset to first page
});
it('should handle empty search query', () => {
const state = { ...initialState, searchQuery: 'existing search' };
const action = setSearchQuery('');
const result = aiPredictionReducer(state, action);
expect(result.searchQuery).toBe('');
expect(result.currentPage).toBe(1);
});
});
// ============================================================================
// FILTER TESTS
// ============================================================================
describe('Filter functionality', () => {
it('should handle setUrgencyFilter', () => {
const filter = 'urgent';
const action = setUrgencyFilter(filter);
const result = aiPredictionReducer(initialState, action);
expect(result.selectedUrgencyFilter).toBe(filter);
expect(result.currentPage).toBe(1); // Should reset to first page
});
it('should handle setSeverityFilter', () => {
const filter = 'high';
const action = setSeverityFilter(filter);
const result = aiPredictionReducer(initialState, action);
expect(result.selectedSeverityFilter).toBe(filter);
expect(result.currentPage).toBe(1);
});
it('should handle setCategoryFilter', () => {
const filter = 'critical';
const action = setCategoryFilter(filter);
const result = aiPredictionReducer(initialState, action);
expect(result.selectedCategoryFilter).toBe(filter);
expect(result.currentPage).toBe(1);
});
it('should handle clearAllFilters', () => {
const state: AIPredictionState = {
...initialState,
searchQuery: 'test',
selectedUrgencyFilter: 'urgent',
selectedSeverityFilter: 'high',
selectedCategoryFilter: 'critical',
currentPage: 3,
};
const action = clearAllFilters();
const result = aiPredictionReducer(state, action);
expect(result.searchQuery).toBe('');
expect(result.selectedUrgencyFilter).toBe('all');
expect(result.selectedSeverityFilter).toBe('all');
expect(result.selectedCategoryFilter).toBe('all');
expect(result.currentPage).toBe(1);
});
});
// ============================================================================
// UI STATE TESTS
// ============================================================================
describe('UI state functionality', () => {
it('should handle toggleShowFilters', () => {
const action = toggleShowFilters();
// Toggle from false to true
const result1 = aiPredictionReducer(initialState, action);
expect(result1.showFilters).toBe(true);
// Toggle from true to false
const result2 = aiPredictionReducer(result1, action);
expect(result2.showFilters).toBe(false);
});
it('should handle clearError', () => {
const state = { ...initialState, error: 'Test error' };
const action = clearError();
const result = aiPredictionReducer(state, action);
expect(result.error).toBe(null);
});
});
// ============================================================================
// ASYNC ACTION TESTS
// ============================================================================
describe('Async actions', () => {
it('should handle fetchAIPredictions.pending', () => {
const action = { type: 'aiPrediction/fetchAIPredictions/pending' };
const result = aiPredictionReducer(initialState, action);
expect(result.isLoading).toBe(true);
expect(result.error).toBe(null);
});
it('should handle fetchAIPredictions.fulfilled', () => {
const mockCases = [
{
patid: 'test-001',
hospital_id: 'hospital-001',
prediction: {
label: 'test finding',
finding_type: 'pathology' as const,
clinical_urgency: 'urgent' as const,
confidence_score: 0.95,
finding_category: 'abnormal' as const,
primary_severity: 'high' as const,
anatomical_location: 'brain',
},
},
];
const action = {
type: 'aiPrediction/fetchAIPredictions/fulfilled',
payload: {
cases: mockCases,
total: 1,
page: 1,
limit: 20,
},
};
const result = aiPredictionReducer(initialState, action);
expect(result.isLoading).toBe(false);
expect(result.predictionCases).toEqual(mockCases);
expect(result.totalItems).toBe(1);
expect(result.error).toBe(null);
expect(result.lastUpdated).toBeInstanceOf(Date);
expect(result.cacheExpiry).toBeInstanceOf(Date);
});
it('should handle fetchAIPredictions.rejected', () => {
const errorMessage = 'Failed to fetch predictions';
const action = {
type: 'aiPrediction/fetchAIPredictions/rejected',
payload: errorMessage,
};
const result = aiPredictionReducer(initialState, action);
expect(result.isLoading).toBe(false);
expect(result.error).toBe(errorMessage);
});
});
});
/*
* End of File: aiPredictionSlice.test.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,522 @@
/*
* File: AIPredictionCard.tsx
* Description: Card component for displaying AI prediction case information
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme';
import { AIPredictionCase, URGENCY_COLORS, SEVERITY_COLORS, CATEGORY_COLORS } from '../types';
// ============================================================================
// INTERFACES
// ============================================================================
interface AIPredictionCardProps {
predictionCase: AIPredictionCase;
onPress: (predictionCase: AIPredictionCase) => void;
onReview?: (caseId: string) => void;
isSelected?: boolean;
onToggleSelect?: (caseId: string) => void;
showReviewButton?: boolean;
}
// ============================================================================
// CONSTANTS
// ============================================================================
const { width } = Dimensions.get('window');
const CARD_WIDTH = width - 32; // Full width with margins
// ============================================================================
// AI PREDICTION CARD COMPONENT
// ============================================================================
/**
* AIPredictionCard Component
*
* Purpose: Display AI prediction case information in a card format
*
* Features:
* - Patient ID and hospital information
* - AI prediction results with confidence score
* - Urgency and severity indicators
* - Review status and actions
* - Selection support for bulk operations
* - Modern card design with proper spacing
* - Color-coded priority indicators
* - Accessibility support
*/
const AIPredictionCard: React.FC<AIPredictionCardProps> = ({
predictionCase,
onPress,
onReview,
isSelected = false,
onToggleSelect,
showReviewButton = true,
}) => {
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Get Urgency Color
*
* Purpose: Get color based on clinical urgency
*/
const getUrgencyColor = (urgency: string): string => {
return URGENCY_COLORS[urgency as keyof typeof URGENCY_COLORS] || theme.colors.textMuted;
};
/**
* Get Severity Color
*
* Purpose: Get color based on primary severity
*/
const getSeverityColor = (severity: string): string => {
return SEVERITY_COLORS[severity as keyof typeof SEVERITY_COLORS] || theme.colors.textMuted;
};
/**
* Get Category Color
*
* Purpose: Get color based on finding category
*/
const getCategoryColor = (category: string): string => {
return CATEGORY_COLORS[category as keyof typeof CATEGORY_COLORS] || theme.colors.textMuted;
};
/**
* Get Review Status Color
*
* Purpose: Get color based on review status
*/
const getReviewStatusColor = (status: string): string => {
switch (status) {
case 'confirmed':
return theme.colors.success;
case 'reviewed':
return theme.colors.info;
case 'disputed':
return theme.colors.warning;
case 'pending':
default:
return theme.colors.error;
}
};
/**
* Format Confidence Score
*
* Purpose: Format confidence score as percentage
*/
const formatConfidence = (score: number): string => {
return `${Math.round(score * 100)}%`;
};
/**
* Capitalize Text
*
* Purpose: Capitalize first letter of each word
*/
const capitalize = (text: string): string => {
return text.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
};
/**
* Format Date
*
* Purpose: Format date for display
*/
const formatDate = (dateString?: string): string => {
if (!dateString) return 'N/A';
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
} catch {
return 'N/A';
}
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Card Press
*
* Purpose: Handle card tap to view details
*/
const handleCardPress = () => {
onPress(predictionCase);
};
/**
* Handle Review Press
*
* Purpose: Handle review button press
*/
const handleReviewPress = (event: any) => {
event.stopPropagation();
if (onReview) {
onReview(predictionCase.patid);
}
};
/**
* Handle Selection Toggle
*
* Purpose: Handle case selection toggle
*/
const handleSelectionToggle = (event: any) => {
event.stopPropagation();
if (onToggleSelect) {
onToggleSelect(predictionCase.patid);
}
};
// ============================================================================
// RENDER
// ============================================================================
return (
<TouchableOpacity
style={[
styles.container,
isSelected && styles.selectedContainer,
predictionCase.prediction.clinical_urgency === 'emergency' && styles.emergencyContainer,
]}
onPress={handleCardPress}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityLabel={`AI Prediction case for patient ${predictionCase.patid}`}
accessibilityHint="Tap to view detailed prediction information"
>
{/* Header Section */}
<View style={styles.header}>
<View style={styles.headerLeft}>
<Text style={styles.patientId} numberOfLines={1}>
{predictionCase.patid}
</Text>
<Text style={styles.date}>
{formatDate(predictionCase.processed_at)}
</Text>
</View>
<View style={styles.headerRight}>
{onToggleSelect && (
<TouchableOpacity
style={styles.selectionButton}
onPress={handleSelectionToggle}
accessibilityRole="checkbox"
accessibilityState={{ checked: isSelected }}
>
<Icon
name={isSelected ? 'check-square' : 'square'}
size={20}
color={isSelected ? theme.colors.primary : theme.colors.textMuted}
/>
</TouchableOpacity>
)}
<View style={[
styles.priorityBadge,
{ backgroundColor: getUrgencyColor(predictionCase.prediction.clinical_urgency) }
]}>
<Text style={styles.priorityText}>
{capitalize(predictionCase.prediction.clinical_urgency)}
</Text>
</View>
</View>
</View>
{/* Prediction Information */}
<View style={styles.predictionSection}>
<View style={styles.predictionHeader}>
<Text style={styles.predictionLabel} numberOfLines={2}>
{capitalize(predictionCase.prediction.label)}
</Text>
<View style={styles.confidenceContainer}>
<Icon name="trending-up" size={16} color={theme.colors.primary} />
<Text style={styles.confidenceText}>
{formatConfidence(predictionCase.prediction.confidence_score)}
</Text>
</View>
</View>
{/* Finding Details */}
<View style={styles.findingDetails}>
<View style={styles.findingItem}>
<Text style={styles.findingLabel}>Type:</Text>
<Text style={styles.findingValue}>
{capitalize(predictionCase.prediction.finding_type)}
</Text>
</View>
<View style={styles.findingItem}>
<Text style={styles.findingLabel}>Category:</Text>
<View style={[
styles.categoryBadge,
{ backgroundColor: getCategoryColor(predictionCase.prediction.finding_category) }
]}>
<Text style={styles.categoryText}>
{capitalize(predictionCase.prediction.finding_category)}
</Text>
</View>
</View>
</View>
{/* Severity and Location */}
<View style={styles.detailsRow}>
<View style={styles.detailItem}>
<Icon name="alert-triangle" size={14} color={getSeverityColor(predictionCase.prediction.primary_severity)} />
<Text style={[styles.detailText, { color: getSeverityColor(predictionCase.prediction.primary_severity) }]}>
{capitalize(predictionCase.prediction.primary_severity)} Severity
</Text>
</View>
{predictionCase.prediction.anatomical_location !== 'not_applicable' && (
<View style={styles.detailItem}>
<Icon name="map-pin" size={14} color={theme.colors.textSecondary} />
<Text style={styles.detailText}>
{capitalize(predictionCase.prediction.anatomical_location)}
</Text>
</View>
)}
</View>
</View>
{/* Footer Section */}
<View style={styles.footer}>
<View style={styles.footerLeft}>
<View style={[
styles.reviewStatusBadge,
{ backgroundColor: getReviewStatusColor(predictionCase.review_status || 'pending') }
]}>
<Text style={styles.reviewStatusText}>
{capitalize(predictionCase.review_status || 'pending')}
</Text>
</View>
{predictionCase.reviewed_by && (
<Text style={styles.reviewedBy}>
by {predictionCase.reviewed_by}
</Text>
)}
</View>
{showReviewButton && predictionCase.review_status === 'pending' && (
<TouchableOpacity
style={styles.reviewButton}
onPress={handleReviewPress}
accessibilityRole="button"
accessibilityLabel="Review this case"
>
<Icon name="eye" size={16} color={theme.colors.primary} />
<Text style={styles.reviewButtonText}>Review</Text>
</TouchableOpacity>
)}
</View>
</TouchableOpacity>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.large,
padding: theme.spacing.lg,
marginHorizontal: theme.spacing.md,
marginVertical: theme.spacing.sm,
width: CARD_WIDTH,
...theme.shadows.medium,
borderWidth: 1,
borderColor: theme.colors.border,
},
selectedContainer: {
borderColor: theme.colors.primary,
borderWidth: 2,
},
emergencyContainer: {
borderLeftWidth: 4,
borderLeftColor: URGENCY_COLORS.emergency,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: theme.spacing.md,
},
headerLeft: {
flex: 1,
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.sm,
},
patientId: {
fontSize: theme.typography.fontSize.bodyLarge,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
date: {
fontSize: theme.typography.fontSize.bodySmall,
color: theme.colors.textSecondary,
},
selectionButton: {
padding: theme.spacing.xs,
},
priorityBadge: {
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.small,
},
priorityText: {
fontSize: theme.typography.fontSize.caption,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.background,
},
predictionSection: {
marginBottom: theme.spacing.md,
},
predictionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'flex-start',
marginBottom: theme.spacing.sm,
},
predictionLabel: {
fontSize: theme.typography.fontSize.bodyLarge,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.textPrimary,
flex: 1,
marginRight: theme.spacing.sm,
},
confidenceContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
},
confidenceText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.primary,
},
findingDetails: {
marginBottom: theme.spacing.sm,
},
findingItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: theme.spacing.xs,
},
findingLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
fontWeight: theme.typography.fontWeight.medium,
},
findingValue: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textPrimary,
},
categoryBadge: {
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.small,
},
categoryText: {
fontSize: theme.typography.fontSize.caption,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.background,
},
detailsRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
detailItem: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
flex: 1,
},
detailText: {
fontSize: theme.typography.fontSize.bodySmall,
color: theme.colors.textSecondary,
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingTop: theme.spacing.sm,
borderTopWidth: 1,
borderTopColor: theme.colors.border,
},
footerLeft: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.sm,
flex: 1,
},
reviewStatusBadge: {
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.small,
},
reviewStatusText: {
fontSize: theme.typography.fontSize.caption,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.background,
},
reviewedBy: {
fontSize: theme.typography.fontSize.caption,
color: theme.colors.textMuted,
fontStyle: 'italic',
},
reviewButton: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.small,
borderWidth: 1,
borderColor: theme.colors.primary,
},
reviewButtonText: {
fontSize: theme.typography.fontSize.bodySmall,
color: theme.colors.primary,
fontWeight: theme.typography.fontWeight.medium,
},
});
export default AIPredictionCard;
/*
* End of File: AIPredictionCard.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,287 @@
/*
* File: EmptyState.tsx
* Description: Empty state component for AI predictions
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme';
// ============================================================================
// INTERFACES
// ============================================================================
interface EmptyStateProps {
title?: string;
message?: string;
iconName?: string;
actionText?: string;
onAction?: () => void;
style?: any;
showRefreshButton?: boolean;
onRefresh?: () => void;
}
// ============================================================================
// CONSTANTS
// ============================================================================
const { width, height } = Dimensions.get('window');
// ============================================================================
// EMPTY STATE COMPONENT
// ============================================================================
/**
* EmptyState Component
*
* Purpose: Display empty state for AI predictions
*
* Features:
* - Customizable title and message
* - Icon display with customizable icon
* - Optional action button
* - Refresh functionality
* - Responsive design
* - Modern empty state design
* - Accessibility support
*/
const EmptyState: React.FC<EmptyStateProps> = ({
title = 'No AI Predictions Found',
message = 'There are no AI prediction cases available at the moment. Try adjusting your filters or refresh to see new predictions.',
iconName = 'brain',
actionText = 'Refresh',
onAction,
style,
showRefreshButton = true,
onRefresh,
}) => {
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Action Press
*
* Purpose: Handle action button press
*/
const handleActionPress = () => {
if (onAction) {
onAction();
} else if (onRefresh) {
onRefresh();
}
};
// ============================================================================
// RENDER
// ============================================================================
return (
<View style={[styles.container, style]}>
{/* Empty State Icon */}
<View style={styles.iconContainer}>
<Icon
name={iconName}
size={64}
color={theme.colors.textMuted}
style={styles.icon}
/>
</View>
{/* Empty State Title */}
<Text style={styles.title} accessibilityRole="header">
{title}
</Text>
{/* Empty State Message */}
<Text style={styles.message}>
{message}
</Text>
{/* Action Buttons */}
<View style={styles.buttonsContainer}>
{/* Primary Action Button */}
{(onAction || onRefresh) && (
<TouchableOpacity
style={styles.actionButton}
onPress={handleActionPress}
accessibilityRole="button"
accessibilityLabel={actionText}
>
<Icon
name="refresh-cw"
size={18}
color={theme.colors.background}
style={styles.buttonIcon}
/>
<Text style={styles.actionButtonText}>
{actionText}
</Text>
</TouchableOpacity>
)}
{/* Secondary Refresh Button */}
{showRefreshButton && onRefresh && !onAction && (
<TouchableOpacity
style={styles.secondaryButton}
onPress={onRefresh}
accessibilityRole="button"
accessibilityLabel="Refresh data"
>
<Icon
name="refresh-cw"
size={16}
color={theme.colors.primary}
style={styles.buttonIcon}
/>
<Text style={styles.secondaryButtonText}>
Refresh Data
</Text>
</TouchableOpacity>
)}
</View>
{/* Suggestions */}
<View style={styles.suggestionsContainer}>
<Text style={styles.suggestionsTitle}>Try:</Text>
<View style={styles.suggestionsList}>
<View style={styles.suggestionItem}>
<Icon name="search" size={14} color={theme.colors.textMuted} />
<Text style={styles.suggestionText}>Clearing search filters</Text>
</View>
<View style={styles.suggestionItem}>
<Icon name="filter" size={14} color={theme.colors.textMuted} />
<Text style={styles.suggestionText}>Adjusting filter criteria</Text>
</View>
<View style={styles.suggestionItem}>
<Icon name="refresh-cw" size={14} color={theme.colors.textMuted} />
<Text style={styles.suggestionText}>Refreshing the data</Text>
</View>
</View>
</View>
</View>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: theme.spacing.xl,
paddingVertical: theme.spacing.xxl,
minHeight: height * 0.4,
},
iconContainer: {
width: 120,
height: 120,
borderRadius: 60,
backgroundColor: theme.colors.backgroundAlt,
justifyContent: 'center',
alignItems: 'center',
marginBottom: theme.spacing.xl,
...theme.shadows.small,
},
icon: {
opacity: 0.6,
},
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
textAlign: 'center',
marginBottom: theme.spacing.md,
},
message: {
fontSize: theme.typography.fontSize.bodyLarge,
color: theme.colors.textSecondary,
textAlign: 'center',
lineHeight: theme.typography.lineHeight.relaxed * theme.typography.fontSize.bodyLarge,
marginBottom: theme.spacing.xl,
maxWidth: width * 0.8,
},
buttonsContainer: {
flexDirection: 'row',
gap: theme.spacing.md,
marginBottom: theme.spacing.xl,
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.primary,
paddingHorizontal: theme.spacing.lg,
paddingVertical: theme.spacing.md,
borderRadius: theme.borderRadius.medium,
gap: theme.spacing.sm,
...theme.shadows.medium,
},
actionButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.background,
},
secondaryButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: theme.colors.primary,
paddingHorizontal: theme.spacing.lg,
paddingVertical: theme.spacing.md,
borderRadius: theme.borderRadius.medium,
gap: theme.spacing.sm,
},
secondaryButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.primary,
},
buttonIcon: {
// No additional styles needed
},
suggestionsContainer: {
alignItems: 'center',
maxWidth: width * 0.8,
},
suggestionsTitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.sm,
},
suggestionsList: {
gap: theme.spacing.sm,
},
suggestionItem: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
},
suggestionText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textMuted,
},
});
export default EmptyState;
/*
* End of File: EmptyState.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,368 @@
/*
* File: FilterTabs.tsx
* Description: Filter tabs component for AI predictions
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Dimensions,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme';
import type { AIPredictionState } from '../types';
// ============================================================================
// INTERFACES
// ============================================================================
interface FilterTabsProps {
selectedUrgencyFilter: AIPredictionState['selectedUrgencyFilter'];
selectedSeverityFilter: AIPredictionState['selectedSeverityFilter'];
selectedCategoryFilter: AIPredictionState['selectedCategoryFilter'];
onUrgencyFilterChange: (filter: AIPredictionState['selectedUrgencyFilter']) => void;
onSeverityFilterChange: (filter: AIPredictionState['selectedSeverityFilter']) => void;
onCategoryFilterChange: (filter: AIPredictionState['selectedCategoryFilter']) => void;
onClearFilters: () => void;
filterCounts?: {
urgency: Record<string, number>;
severity: Record<string, number>;
category: Record<string, number>;
};
activeFiltersCount?: number;
}
interface FilterOption {
label: string;
value: string;
count?: number;
color?: string;
}
// ============================================================================
// CONSTANTS
// ============================================================================
const { width } = Dimensions.get('window');
const URGENCY_FILTERS: FilterOption[] = [
{ label: 'All', value: 'all' },
{ label: 'Emergency', value: 'emergency', color: '#F44336' },
{ label: 'Urgent', value: 'urgent', color: '#FF5722' },
{ label: 'Moderate', value: 'moderate', color: '#FF9800' },
{ label: 'Low', value: 'low', color: '#FFC107' },
{ label: 'Routine', value: 'routine', color: '#4CAF50' },
];
const SEVERITY_FILTERS: FilterOption[] = [
{ label: 'All', value: 'all' },
{ label: 'High', value: 'high', color: '#F44336' },
{ label: 'Medium', value: 'medium', color: '#FF9800' },
{ label: 'Low', value: 'low', color: '#FFC107' },
{ label: 'None', value: 'none', color: '#4CAF50' },
];
const CATEGORY_FILTERS: FilterOption[] = [
{ label: 'All', value: 'all' },
{ label: 'Critical', value: 'critical', color: '#F44336' },
{ label: 'Abnormal', value: 'abnormal', color: '#FF9800' },
{ label: 'Warning', value: 'warning', color: '#FFC107' },
{ label: 'Normal', value: 'normal', color: '#4CAF50' },
{ label: 'Unknown', value: 'unknown', color: '#9E9E9E' },
];
// ============================================================================
// FILTER TABS COMPONENT
// ============================================================================
/**
* FilterTabs Component
*
* Purpose: Provide filtering functionality for AI predictions
*
* Features:
* - Multiple filter categories (urgency, severity, category)
* - Visual filter counts
* - Active filter indicators
* - Clear all filters functionality
* - Color-coded filter options
* - Horizontal scroll support
* - Responsive design
* - Accessibility support
*/
const FilterTabs: React.FC<FilterTabsProps> = ({
selectedUrgencyFilter,
selectedSeverityFilter,
selectedCategoryFilter,
onUrgencyFilterChange,
onSeverityFilterChange,
onCategoryFilterChange,
onClearFilters,
filterCounts,
activeFiltersCount = 0,
}) => {
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Get Filter Count
*
* Purpose: Get count for specific filter value
*/
const getFilterCount = (category: 'urgency' | 'severity' | 'category', value: string): number => {
return filterCounts?.[category]?.[value] || 0;
};
/**
* Render Filter Tab
*
* Purpose: Render individual filter tab
*/
const renderFilterTab = (
option: FilterOption,
isSelected: boolean,
onPress: () => void,
category: 'urgency' | 'severity' | 'category'
) => {
const count = getFilterCount(category, option.value);
return (
<TouchableOpacity
key={option.value}
style={[
styles.filterTab,
isSelected && styles.selectedFilterTab,
isSelected && option.color && { borderColor: option.color },
]}
onPress={onPress}
accessibilityRole="button"
accessibilityState={{ selected: isSelected }}
accessibilityLabel={`Filter by ${option.label}${count > 0 ? `, ${count} items` : ''}`}
>
{option.color && isSelected && (
<View style={[styles.colorIndicator, { backgroundColor: option.color }]} />
)}
<Text style={[
styles.filterTabText,
isSelected && styles.selectedFilterTabText,
isSelected && option.color && { color: option.color },
]}>
{option.label}
</Text>
{count > 0 && (
<View style={[
styles.countBadge,
isSelected && styles.selectedCountBadge,
isSelected && option.color && { backgroundColor: option.color },
]}>
<Text style={[
styles.countText,
isSelected && styles.selectedCountText,
]}>
{count}
</Text>
</View>
)}
</TouchableOpacity>
);
};
// ============================================================================
// RENDER
// ============================================================================
return (
<View style={styles.container}>
{/* Header with Clear Filters */}
<View style={styles.header}>
<Text style={styles.headerTitle}>Filters</Text>
{activeFiltersCount > 0 && (
<TouchableOpacity
style={styles.clearButton}
onPress={onClearFilters}
accessibilityRole="button"
accessibilityLabel="Clear all filters"
>
<Icon name="x" size={16} color={theme.colors.primary} />
<Text style={styles.clearButtonText}>Clear All</Text>
</TouchableOpacity>
)}
</View>
{/* Urgency Filters */}
<View style={styles.filterSection}>
<Text style={styles.sectionTitle}>Clinical Urgency</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.filterRow}
>
{URGENCY_FILTERS.map((option) =>
renderFilterTab(
{ ...option, count: getFilterCount('urgency', option.value) },
selectedUrgencyFilter === option.value,
() => onUrgencyFilterChange(option.value as AIPredictionState['selectedUrgencyFilter']),
'urgency'
)
)}
</ScrollView>
</View>
{/* Severity Filters */}
<View style={styles.filterSection}>
<Text style={styles.sectionTitle}>Primary Severity</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.filterRow}
>
{SEVERITY_FILTERS.map((option) =>
renderFilterTab(
{ ...option, count: getFilterCount('severity', option.value) },
selectedSeverityFilter === option.value,
() => onSeverityFilterChange(option.value as AIPredictionState['selectedSeverityFilter']),
'severity'
)
)}
</ScrollView>
</View>
{/* Category Filters */}
<View style={styles.filterSection}>
<Text style={styles.sectionTitle}>Finding Category</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.filterRow}
>
{CATEGORY_FILTERS.map((option) =>
renderFilterTab(
{ ...option, count: getFilterCount('category', option.value) },
selectedCategoryFilter === option.value,
() => onCategoryFilterChange(option.value as AIPredictionState['selectedCategoryFilter']),
'category'
)
)}
</ScrollView>
</View>
</View>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
paddingVertical: theme.spacing.md,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
marginBottom: theme.spacing.md,
},
headerTitle: {
fontSize: theme.typography.fontSize.bodyLarge,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
},
clearButton: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
borderRadius: theme.borderRadius.small,
borderWidth: 1,
borderColor: theme.colors.primary,
},
clearButtonText: {
fontSize: theme.typography.fontSize.bodySmall,
color: theme.colors.primary,
fontWeight: theme.typography.fontWeight.medium,
},
filterSection: {
marginBottom: theme.spacing.lg,
},
sectionTitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontWeight: theme.typography.fontWeight.medium,
color: theme.colors.textSecondary,
paddingHorizontal: theme.spacing.md,
marginBottom: theme.spacing.sm,
},
filterRow: {
paddingHorizontal: theme.spacing.md,
gap: theme.spacing.sm,
},
filterTab: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
borderWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.background,
gap: theme.spacing.xs,
},
selectedFilterTab: {
borderColor: theme.colors.primary,
backgroundColor: theme.colors.backgroundAccent,
},
colorIndicator: {
width: 8,
height: 8,
borderRadius: 4,
},
filterTabText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
fontWeight: theme.typography.fontWeight.medium,
},
selectedFilterTabText: {
color: theme.colors.primary,
fontWeight: theme.typography.fontWeight.bold,
},
countBadge: {
backgroundColor: theme.colors.textMuted,
borderRadius: theme.borderRadius.small,
paddingHorizontal: theme.spacing.xs,
paddingVertical: 2,
minWidth: 20,
alignItems: 'center',
},
selectedCountBadge: {
backgroundColor: theme.colors.primary,
},
countText: {
fontSize: theme.typography.fontSize.caption,
color: theme.colors.background,
fontWeight: theme.typography.fontWeight.bold,
},
selectedCountText: {
color: theme.colors.background,
},
});
export default FilterTabs;
/*
* End of File: FilterTabs.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,139 @@
/*
* File: LoadingState.tsx
* Description: Loading state component for AI predictions
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import {
View,
Text,
StyleSheet,
ActivityIndicator,
Dimensions,
} from 'react-native';
import { theme } from '../../../theme';
// ============================================================================
// INTERFACES
// ============================================================================
interface LoadingStateProps {
message?: string;
showSpinner?: boolean;
size?: 'small' | 'large';
style?: any;
}
// ============================================================================
// CONSTANTS
// ============================================================================
const { width, height } = Dimensions.get('window');
// ============================================================================
// LOADING STATE COMPONENT
// ============================================================================
/**
* LoadingState Component
*
* Purpose: Display loading state for AI predictions
*
* Features:
* - Customizable loading message
* - Optional spinner display
* - Different spinner sizes
* - Custom styling support
* - Centered layout
* - Accessibility support
*/
const LoadingState: React.FC<LoadingStateProps> = ({
message = 'Loading AI predictions...',
showSpinner = true,
size = 'large',
style,
}) => {
// ============================================================================
// RENDER
// ============================================================================
return (
<View style={[styles.container, style]} accessibilityRole="progressbar">
{/* Loading Spinner */}
{showSpinner && (
<ActivityIndicator
size={size}
color={theme.colors.primary}
style={styles.spinner}
/>
)}
{/* Loading Message */}
<Text style={styles.message} accessibilityLabel={message}>
{message}
</Text>
{/* Loading Animation Dots */}
<View style={styles.dotsContainer}>
<View style={[styles.dot, styles.dot1]} />
<View style={[styles.dot, styles.dot2]} />
<View style={[styles.dot, styles.dot3]} />
</View>
</View>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: theme.spacing.xl,
paddingVertical: theme.spacing.xxl,
minHeight: height * 0.3,
},
spinner: {
marginBottom: theme.spacing.lg,
},
message: {
fontSize: theme.typography.fontSize.bodyLarge,
color: theme.colors.textSecondary,
textAlign: 'center',
fontWeight: theme.typography.fontWeight.medium,
marginBottom: theme.spacing.xl,
},
dotsContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.sm,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: theme.colors.primary,
},
dot1: {
opacity: 0.3,
},
dot2: {
opacity: 0.6,
},
dot3: {
opacity: 1,
},
});
export default LoadingState;
/*
* End of File: LoadingState.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,226 @@
/*
* File: SearchBar.tsx
* Description: Search bar component for filtering AI predictions
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState, useCallback } from 'react';
import {
View,
TextInput,
StyleSheet,
TouchableOpacity,
Dimensions,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme';
// ============================================================================
// INTERFACES
// ============================================================================
interface SearchBarProps {
value: string;
onChangeText: (text: string) => void;
onClear?: () => void;
placeholder?: string;
autoFocus?: boolean;
disabled?: boolean;
}
// ============================================================================
// CONSTANTS
// ============================================================================
const { width } = Dimensions.get('window');
// ============================================================================
// SEARCH BAR COMPONENT
// ============================================================================
/**
* SearchBar Component
*
* Purpose: Provide search functionality for AI predictions
*
* Features:
* - Real-time search input
* - Clear button functionality
* - Customizable placeholder text
* - Auto-focus support
* - Disabled state support
* - Modern design with icons
* - Responsive width
* - Accessibility support
*/
const SearchBar: React.FC<SearchBarProps> = ({
value,
onChangeText,
onClear,
placeholder = 'Search predictions...',
autoFocus = false,
disabled = false,
}) => {
// ============================================================================
// STATE
// ============================================================================
const [isFocused, setIsFocused] = useState(false);
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Focus
*
* Purpose: Handle input focus state
*/
const handleFocus = useCallback(() => {
setIsFocused(true);
}, []);
/**
* Handle Blur
*
* Purpose: Handle input blur state
*/
const handleBlur = useCallback(() => {
setIsFocused(false);
}, []);
/**
* Handle Clear
*
* Purpose: Clear search input
*/
const handleClear = useCallback(() => {
onChangeText('');
if (onClear) {
onClear();
}
}, [onChangeText, onClear]);
/**
* Handle Text Change
*
* Purpose: Handle search text input
*/
const handleTextChange = useCallback((text: string) => {
onChangeText(text);
}, [onChangeText]);
// ============================================================================
// RENDER
// ============================================================================
return (
<View style={[
styles.container,
isFocused && styles.focusedContainer,
disabled && styles.disabledContainer,
]}>
{/* Search Icon */}
<Icon
name="search"
size={20}
color={isFocused ? theme.colors.primary : theme.colors.textMuted}
style={styles.searchIcon}
/>
{/* Text Input */}
<TextInput
style={[
styles.input,
disabled && styles.disabledInput,
]}
value={value}
onChangeText={handleTextChange}
onFocus={handleFocus}
onBlur={handleBlur}
placeholder={placeholder}
placeholderTextColor={theme.colors.textMuted}
autoFocus={autoFocus}
editable={!disabled}
selectTextOnFocus={!disabled}
autoCorrect={false}
autoCapitalize="none"
returnKeyType="search"
clearButtonMode="never" // We handle clear button manually
accessibilityLabel="Search AI predictions"
accessibilityHint="Enter patient ID, finding type, or location to search"
/>
{/* Clear Button */}
{value.length > 0 && !disabled && (
<TouchableOpacity
style={styles.clearButton}
onPress={handleClear}
accessibilityRole="button"
accessibilityLabel="Clear search"
accessibilityHint="Clear the search input"
>
<Icon
name="x"
size={18}
color={theme.colors.textMuted}
/>
</TouchableOpacity>
)}
</View>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.background,
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
marginHorizontal: theme.spacing.md,
marginVertical: theme.spacing.sm,
...theme.shadows.small,
},
focusedContainer: {
borderColor: theme.colors.primary,
backgroundColor: theme.colors.background,
},
disabledContainer: {
backgroundColor: theme.colors.backgroundAlt,
opacity: 0.6,
},
searchIcon: {
marginRight: theme.spacing.sm,
},
input: {
flex: 1,
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textPrimary,
paddingVertical: 0, // Remove default padding to maintain consistent height
fontFamily: theme.typography.fontFamily.regular,
},
disabledInput: {
color: theme.colors.textMuted,
},
clearButton: {
marginLeft: theme.spacing.sm,
padding: theme.spacing.xs,
},
});
export default SearchBar;
/*
* End of File: SearchBar.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,454 @@
/*
* File: StatsOverview.tsx
* Description: Statistics overview component for AI predictions dashboard
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Dimensions,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme';
import type { AIPredictionStats } from '../types';
// ============================================================================
// INTERFACES
// ============================================================================
interface StatsOverviewProps {
stats: AIPredictionStats;
onStatsPress?: (statType: string) => void;
isLoading?: boolean;
style?: any;
}
interface StatCardProps {
title: string;
value: string | number;
subtitle?: string;
iconName: string;
color: string;
onPress?: () => void;
trend?: number;
isPercentage?: boolean;
}
// ============================================================================
// CONSTANTS
// ============================================================================
const { width } = Dimensions.get('window');
const CARD_WIDTH = (width - 48) / 2; // Two cards per row with margins
// ============================================================================
// STAT CARD COMPONENT
// ============================================================================
/**
* StatCard Component
*
* Purpose: Individual statistics card
*/
const StatCard: React.FC<StatCardProps> = ({
title,
value,
subtitle,
iconName,
color,
onPress,
trend,
isPercentage = false,
}) => {
const displayValue = typeof value === 'number'
? isPercentage
? `${Math.round(value * 100)}%`
: value.toLocaleString()
: value;
return (
<TouchableOpacity
style={[styles.statCard, { borderLeftColor: color }]}
onPress={onPress}
disabled={!onPress}
accessibilityRole="button"
accessibilityLabel={`${title}: ${displayValue}${subtitle ? `, ${subtitle}` : ''}`}
>
{/* Card Header */}
<View style={styles.cardHeader}>
<View style={{flexDirection: 'row', alignItems: 'center', gap: theme.spacing.sm}}>
<View style={[styles.iconContainer, { backgroundColor: color + '20' }]}>
<Icon name={iconName} size={20} color={color} />
</View>
<Text style={styles.statValue}>{displayValue}</Text>
</View>
{trend !== undefined && (
<View style={styles.trendContainer}>
<Icon
name={trend >= 0 ? 'trending-up' : 'trending-down'}
size={14}
color={trend >= 0 ? theme.colors.success : theme.colors.error}
/>
<Text style={[
styles.trendText,
{ color: trend >= 0 ? theme.colors.success : theme.colors.error }
]}>
{Math.abs(trend).toFixed(1)}%
</Text>
</View>
)}
</View>
{/* Card Content */}
<View style={styles.cardContent}>
<Text style={styles.statTitle} numberOfLines={2}>{title}</Text>
{subtitle && (
<Text style={styles.statSubtitle} numberOfLines={1}>{subtitle}</Text>
)}
</View>
</TouchableOpacity>
);
};
// ============================================================================
// STATS OVERVIEW COMPONENT
// ============================================================================
/**
* StatsOverview Component
*
* Purpose: Display comprehensive AI predictions statistics
*
* Features:
* - Total cases overview
* - Critical and urgent case counts
* - Review progress tracking
* - Average confidence metrics
* - Trend indicators
* - Interactive stat cards
* - Responsive grid layout
* - Modern card design
* - Accessibility support
*/
const StatsOverview: React.FC<StatsOverviewProps> = ({
stats,
onStatsPress,
isLoading = false,
style,
}) => {
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Stat Press
*
* Purpose: Handle statistics card press
*/
const handleStatPress = (statType: string) => {
if (onStatsPress) {
onStatsPress(statType);
}
};
// ============================================================================
// RENDER
// ============================================================================
if (isLoading) {
return (
<View style={[styles.container, style]}>
<View style={styles.header}>
<Text style={styles.sectionTitle}>AI Predictions Overview</Text>
</View>
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>Loading statistics...</Text>
</View>
</View>
);
}
return (
<View style={[styles.container, style]}>
{/* Section Header */}
<View style={styles.header}>
<Text style={styles.sectionTitle}>AI Predictions Overview</Text>
<TouchableOpacity
style={styles.viewAllButton}
onPress={() => handleStatPress('all')}
accessibilityRole="button"
accessibilityLabel="View all statistics"
>
<Text style={styles.viewAllText}>View All</Text>
<Icon name="arrow-right" size={16} color={theme.colors.primary} />
</TouchableOpacity>
</View>
{/* Statistics Grid */}
<View style={styles.statsGrid}>
{/* Total Cases */}
<StatCard
title="Total Cases"
value={stats.totalCases}
subtitle="All predictions"
iconName="database"
color={theme.colors.primary}
onPress={() => handleStatPress('total')}
/>
{/* Critical Cases */}
<StatCard
title="Critical Cases"
value={stats.criticalCases}
subtitle="Require attention"
iconName="alert-triangle"
color={theme.colors.error}
onPress={() => handleStatPress('critical')}
/>
{/* Urgent Cases */}
<StatCard
title="Urgent Cases"
value={stats.urgentCases}
subtitle="High priority"
iconName="clock"
color={theme.colors.warning}
onPress={() => handleStatPress('urgent')}
/>
{/* Reviewed Cases */}
<StatCard
title="Reviewed Cases"
value={stats.reviewedCases}
subtitle="Completed reviews"
iconName="check-circle"
color={theme.colors.success}
onPress={() => handleStatPress('reviewed')}
/>
{/* Pending Cases */}
<StatCard
title="Pending Reviews"
value={stats.pendingCases}
subtitle="Awaiting review"
iconName="eye"
color={theme.colors.info}
onPress={() => handleStatPress('pending')}
/>
{/* Average Confidence */}
<StatCard
title="Avg Confidence"
value={stats.averageConfidence}
subtitle="AI accuracy"
iconName="trending-up"
color={theme.colors.primary}
onPress={() => handleStatPress('confidence')}
isPercentage={true}
/>
{/* Today's Cases */}
<StatCard
title="Today's Cases"
value={stats.todaysCases}
subtitle="New predictions"
iconName="calendar"
color={theme.colors.info}
onPress={() => handleStatPress('today')}
/>
{/* Weekly Trend */}
<StatCard
title="Weekly Trend"
value={`${stats.weeklyTrend >= 0 ? '+' : ''}${stats.weeklyTrend.toFixed(1)}%`}
subtitle="vs last week"
iconName={stats.weeklyTrend >= 0 ? 'trending-up' : 'trending-down'}
color={stats.weeklyTrend >= 0 ? theme.colors.success : theme.colors.error}
onPress={() => handleStatPress('trend')}
trend={stats.weeklyTrend}
/>
</View>
{/* Summary Section */}
<View style={styles.summarySection}>
<View style={styles.summaryCard}>
<View style={styles.summaryHeader}>
<Icon name="activity" size={20} color={theme.colors.primary} />
<Text style={styles.summaryTitle}>Quick Insights</Text>
</View>
<View style={styles.summaryContent}>
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}>Review Progress:</Text>
<Text style={styles.summaryValue}>
{Math.round((stats.reviewedCases / stats.totalCases) * 100)}%
</Text>
</View>
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}>Critical Rate:</Text>
<Text style={styles.summaryValue}>
{Math.round((stats.criticalCases / stats.totalCases) * 100)}%
</Text>
</View>
<View style={styles.summaryItem}>
<Text style={styles.summaryLabel}>Daily Average:</Text>
<Text style={styles.summaryValue}>
{Math.round(stats.totalCases / 7)} cases
</Text>
</View>
</View>
</View>
</View>
</View>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
backgroundColor: theme.colors.background,
paddingVertical: theme.spacing.lg,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
marginBottom: theme.spacing.lg,
},
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
},
viewAllButton: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
},
viewAllText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.primary,
fontWeight: theme.typography.fontWeight.medium,
},
loadingContainer: {
paddingVertical: theme.spacing.xxl,
alignItems: 'center',
},
loadingText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textMuted,
},
statsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
paddingHorizontal: theme.spacing.md,
gap: theme.spacing.md,
},
statCard: {
width: CARD_WIDTH,
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
borderLeftWidth: 4,
padding: theme.spacing.md,
...theme.shadows.medium,
},
cardHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: theme.spacing.sm,
},
iconContainer: {
width: 36,
height: 36,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
},
trendContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.xs,
},
trendText: {
fontSize: theme.typography.fontSize.caption,
fontWeight: theme.typography.fontWeight.medium,
},
cardContent: {
gap: theme.spacing.xs,
},
statValue: {
fontSize: theme.typography.fontSize.displayMedium,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
},
statTitle: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
fontWeight: theme.typography.fontWeight.medium,
},
statSubtitle: {
fontSize: theme.typography.fontSize.bodySmall,
color: theme.colors.textMuted,
},
summarySection: {
paddingHorizontal: theme.spacing.md,
marginTop: theme.spacing.lg,
},
summaryCard: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.lg,
...theme.shadows.small,
},
summaryHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: theme.spacing.sm,
marginBottom: theme.spacing.md,
},
summaryTitle: {
fontSize: theme.typography.fontSize.bodyLarge,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
},
summaryContent: {
gap: theme.spacing.sm,
},
summaryItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
summaryLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
},
summaryValue: {
fontSize: theme.typography.fontSize.bodyMedium,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
},
});
export default StatsOverview;
/*
* End of File: StatsOverview.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,19 @@
/*
* File: index.ts
* Description: Components exports for AI Prediction module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export { default as AIPredictionCard } from './AIPredictionCard';
export { default as SearchBar } from './SearchBar';
export { default as FilterTabs } from './FilterTabs';
export { default as LoadingState } from './LoadingState';
export { default as EmptyState } from './EmptyState';
export { default as StatsOverview } from './StatsOverview';
/*
* End of File: index.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

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

View File

@ -0,0 +1,383 @@
/*
* File: useAIPredictions.ts
* Description: Custom hook for AI Predictions functionality
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { useCallback, useEffect, useMemo } from 'react';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
// Import Redux actions and selectors
import {
fetchAIPredictions,
setSearchQuery,
setUrgencyFilter,
setSeverityFilter,
setCategoryFilter,
clearAllFilters,
updateCaseReview,
} from '../redux';
import {
selectPaginatedCases,
selectIsLoading,
selectError,
selectSearchQuery,
selectUrgencyFilter,
selectSeverityFilter,
selectCategoryFilter,
selectCasesStatistics,
selectActiveFiltersCount,
selectCurrentPage,
selectTotalPages,
} from '../redux';
// Import auth selector
import { selectUser } from '../../Auth/redux/authSelectors';
// Import types
import type { AIPredictionState } from '../types';
// ============================================================================
// INTERFACES
// ============================================================================
interface UseAIPredictionsOptions {
autoLoad?: boolean;
refreshInterval?: number;
}
interface UseAIPredictionsReturn {
// Data
cases: ReturnType<typeof selectPaginatedCases>;
statistics: ReturnType<typeof selectCasesStatistics>;
// Loading states
isLoading: boolean;
error: string | null;
// Filters
searchQuery: string;
urgencyFilter: AIPredictionState['selectedUrgencyFilter'];
severityFilter: AIPredictionState['selectedSeverityFilter'];
categoryFilter: AIPredictionState['selectedCategoryFilter'];
activeFiltersCount: number;
// Pagination
currentPage: number;
totalPages: number;
// Actions
loadPredictions: () => Promise<void>;
refreshPredictions: () => Promise<void>;
setSearch: (query: string) => void;
setUrgency: (filter: AIPredictionState['selectedUrgencyFilter']) => void;
setSeverity: (filter: AIPredictionState['selectedSeverityFilter']) => void;
setCategory: (filter: AIPredictionState['selectedCategoryFilter']) => void;
clearFilters: () => void;
reviewCase: (caseId: string, reviewData?: Partial<{
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
reviewed_by: string;
review_notes: string;
priority: 'critical' | 'high' | 'medium' | 'low';
}>) => Promise<void>;
// Computed properties
hasFilters: boolean;
isEmpty: boolean;
hasError: boolean;
}
// ============================================================================
// USE AI PREDICTIONS HOOK
// ============================================================================
/**
* useAIPredictions Hook
*
* Purpose: Custom hook for managing AI predictions state and actions
*
* Features:
* - Automatic data loading on mount
* - Search and filtering functionality
* - Case review management
* - Error handling
* - Loading states
* - Computed properties for UI state
* - Auto-refresh capability
* - Type-safe actions and selectors
*/
export const useAIPredictions = (options: UseAIPredictionsOptions = {}): UseAIPredictionsReturn => {
const {
autoLoad = true,
refreshInterval,
} = options;
// ============================================================================
// REDUX STATE
// ============================================================================
const dispatch = useAppDispatch();
// Auth state
const user = useAppSelector(selectUser);
// AI Predictions state
const cases = useAppSelector(selectPaginatedCases);
const statistics = useAppSelector(selectCasesStatistics);
const isLoading = useAppSelector(selectIsLoading);
const error = useAppSelector(selectError);
const searchQuery = useAppSelector(selectSearchQuery);
const urgencyFilter = useAppSelector(selectUrgencyFilter);
const severityFilter = useAppSelector(selectSeverityFilter);
const categoryFilter = useAppSelector(selectCategoryFilter);
const activeFiltersCount = useAppSelector(selectActiveFiltersCount);
const currentPage = useAppSelector(selectCurrentPage);
const totalPages = useAppSelector(selectTotalPages);
// ============================================================================
// MEMOIZED VALUES
// ============================================================================
/**
* Has Filters
*
* Purpose: Check if any filters are active
*/
const hasFilters = useMemo(() => activeFiltersCount > 0, [activeFiltersCount]);
/**
* Is Empty
*
* Purpose: Check if the cases list is empty
*/
const isEmpty = useMemo(() => cases.length === 0, [cases.length]);
/**
* Has Error
*
* Purpose: Check if there's an error
*/
const hasError = useMemo(() => error !== null, [error]);
// ============================================================================
// ACTIONS
// ============================================================================
/**
* Load Predictions
*
* Purpose: Load AI predictions from API
*/
const loadPredictions = useCallback(async () => {
if (!user?.access_token) {
throw new Error('User not authenticated');
}
try {
const params = {
page: currentPage,
limit: 20,
...(urgencyFilter !== 'all' && { urgency: urgencyFilter }),
...(severityFilter !== 'all' && { severity: severityFilter }),
...(categoryFilter !== 'all' && { category: categoryFilter }),
...(searchQuery.trim() && { search: searchQuery.trim() }),
};
await dispatch(fetchAIPredictions({
token: user.access_token,
params,
})).unwrap();
} catch (error) {
console.error('Failed to load AI predictions:', error);
throw error;
}
}, [
dispatch,
user?.access_token,
currentPage,
urgencyFilter,
severityFilter,
categoryFilter,
searchQuery,
]);
/**
* Refresh Predictions
*
* Purpose: Refresh AI predictions data
*/
const refreshPredictions = useCallback(async () => {
await loadPredictions();
}, [loadPredictions]);
/**
* Set Search
*
* Purpose: Set search query
*/
const setSearch = useCallback((query: string) => {
dispatch(setSearchQuery(query));
}, [dispatch]);
/**
* Set Urgency Filter
*
* Purpose: Set urgency filter
*/
const setUrgency = useCallback((filter: AIPredictionState['selectedUrgencyFilter']) => {
dispatch(setUrgencyFilter(filter));
}, [dispatch]);
/**
* Set Severity Filter
*
* Purpose: Set severity filter
*/
const setSeverity = useCallback((filter: AIPredictionState['selectedSeverityFilter']) => {
dispatch(setSeverityFilter(filter));
}, [dispatch]);
/**
* Set Category Filter
*
* Purpose: Set category filter
*/
const setCategory = useCallback((filter: AIPredictionState['selectedCategoryFilter']) => {
dispatch(setCategoryFilter(filter));
}, [dispatch]);
/**
* Clear Filters
*
* Purpose: Clear all active filters
*/
const clearFilters = useCallback(() => {
dispatch(clearAllFilters());
}, [dispatch]);
/**
* Review Case
*
* Purpose: Update case review status
*/
const reviewCase = useCallback(async (
caseId: string,
reviewData: Partial<{
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
reviewed_by: string;
review_notes: string;
priority: 'critical' | 'high' | 'medium' | 'low';
}> = {}
) => {
if (!user?.access_token) {
throw new Error('User not authenticated');
}
try {
const defaultReviewData = {
review_status: 'reviewed' as const,
reviewed_by: user.display_name || user.email || 'Current User',
...reviewData,
};
await dispatch(updateCaseReview({
caseId,
reviewData: defaultReviewData,
token: user.access_token,
})).unwrap();
} catch (error) {
console.error('Failed to review case:', error);
throw error;
}
}, [dispatch, user]);
// ============================================================================
// EFFECTS
// ============================================================================
/**
* Auto-load Effect
*
* Purpose: Automatically load predictions on mount if enabled
*/
useEffect(() => {
if (autoLoad && user?.access_token) {
loadPredictions().catch(console.error);
}
}, [autoLoad, user?.access_token, loadPredictions]);
/**
* Auto-refresh Effect
*
* Purpose: Set up auto-refresh interval if specified
*/
useEffect(() => {
if (!refreshInterval || !user?.access_token) return;
const interval = setInterval(() => {
loadPredictions().catch(console.error);
}, refreshInterval);
return () => clearInterval(interval);
}, [refreshInterval, user?.access_token, loadPredictions]);
/**
* Filter Change Effect
*
* Purpose: Reload data when filters change
*/
useEffect(() => {
if (user?.access_token) {
loadPredictions().catch(console.error);
}
}, [urgencyFilter, severityFilter, categoryFilter, searchQuery, currentPage]);
// ============================================================================
// RETURN
// ============================================================================
return {
// Data
cases,
statistics,
// Loading states
isLoading,
error,
// Filters
searchQuery,
urgencyFilter,
severityFilter,
categoryFilter,
activeFiltersCount,
// Pagination
currentPage,
totalPages,
// Actions
loadPredictions,
refreshPredictions,
setSearch,
setUrgency,
setSeverity,
setCategory,
clearFilters,
reviewCase,
// Computed properties
hasFilters,
isEmpty,
hasError,
};
};
/*
* End of File: useAIPredictions.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,54 @@
/*
* File: index.ts
* Description: Main exports for AI Prediction module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
// ============================================================================
// COMPONENT EXPORTS
// ============================================================================
export * from './components';
// ============================================================================
// SCREEN EXPORTS
// ============================================================================
export * from './screens';
// ============================================================================
// NAVIGATION EXPORTS
// ============================================================================
export * from './navigation';
// ============================================================================
// REDUX EXPORTS
// ============================================================================
export * from './redux';
// ============================================================================
// SERVICE EXPORTS
// ============================================================================
export * from './services';
// ============================================================================
// TYPE EXPORTS
// ============================================================================
export * from './types';
// ============================================================================
// HOOK EXPORTS
// ============================================================================
export * from './hooks';
/*
* End of File: index.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,249 @@
/*
* File: AIPredictionStackNavigator.tsx
* Description: Stack navigator for AI Prediction module screens
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import { createStackNavigator } from '@react-navigation/stack';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme';
// Import screens
import { AIPredictionsScreen, AIPredictionDetailScreen } from '../screens';
import { ComingSoonScreen, DicomViewer } from '../../../shared/components';
// Import types
import type { AIPredictionStackParamList } from './navigationTypes';
// ============================================================================
// STACK NAVIGATOR SETUP
// ============================================================================
const Stack = createStackNavigator<AIPredictionStackParamList>();
// ============================================================================
// HEADER COMPONENTS
// ============================================================================
/**
* Header Back Button
*
* Purpose: Custom back button for navigation header
*/
const HeaderBackButton: React.FC<{ onPress: () => void }> = ({ onPress }) => (
<TouchableOpacity style={styles.headerButton} onPress={onPress}>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
);
/**
* Header Action Button
*
* Purpose: Custom action button for navigation header
*/
const HeaderActionButton: React.FC<{
iconName: string;
onPress: () => void;
accessibilityLabel?: string;
}> = ({ iconName, onPress, accessibilityLabel }) => (
<TouchableOpacity
style={styles.headerButton}
onPress={onPress}
accessibilityRole="button"
accessibilityLabel={accessibilityLabel}
>
<Icon name={iconName} size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
);
// ============================================================================
// SCREEN OPTIONS
// ============================================================================
/**
* Default Screen Options
*
* Purpose: Common screen options for all AI prediction screens
*/
const defaultScreenOptions = {
headerStyle: {
backgroundColor: theme.colors.background,
elevation: 2,
shadowOpacity: 0.1,
shadowRadius: 4,
shadowOffset: { width: 0, height: 2 },
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
headerTitleStyle: {
fontSize: theme.typography.fontSize.bodyLarge,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
},
headerTintColor: theme.colors.textPrimary,
headerBackTitleVisible: false,
gestureEnabled: true,
cardStyleInterpolator: ({ current, layouts }: any) => {
return {
cardStyle: {
transform: [
{
translateX: current.progress.interpolate({
inputRange: [0, 1],
outputRange: [layouts.screen.width, 0],
}),
},
],
},
};
},
};
// ============================================================================
// AI PREDICTION STACK NAVIGATOR COMPONENT
// ============================================================================
/**
* AIPredictionStackNavigator Component
*
* Purpose: Stack navigator for AI prediction module
*
* Features:
* - AI Prediction List screen (main screen)
* - AI Prediction Details screen (case details)
* - AI Prediction Filters screen (advanced filtering)
* - AI Prediction Stats screen (detailed statistics)
* - Custom header styling and buttons
* - Smooth navigation transitions
* - Accessibility support
* - Coming soon screens for unimplemented features
*/
const AIPredictionStackNavigator: React.FC = () => {
return (
<Stack.Navigator
initialRouteName="AIPredictionList"
screenOptions={defaultScreenOptions}
>
{/* AI Prediction List Screen */}
<Stack.Screen
name="AIPredictionList"
component={AIPredictionsScreen}
options={({ navigation }) => ({
title: 'AI Predictions',
headerLeft: () => null, // No back button on main screen
headerRight: () => (
<HeaderActionButton
iconName="more-vertical"
onPress={() => {
// Open options menu
// For now, just navigate to stats
// @ts-ignore
navigation.navigate('AIPredictionStats');
}}
accessibilityLabel="More options"
/>
),
})}
/>
{/* AI Prediction Details Screen */}
<Stack.Screen
name="AIPredictionDetails"
component={() => <DicomViewer
dicomUrl={'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm'}
debugMode={true}
onError={(error) => console.log('DICOM Error:', error)}
onLoad={() => console.log('DICOM Viewer loaded successfully')}
/>}
options={({ navigation, route }) => ({
title: 'Create Suggestion',
headerLeft: () => (
<HeaderBackButton onPress={() => navigation.goBack()} />
),
headerRight: () => (
<HeaderActionButton
iconName="help-circle"
onPress={() => {
// Show help for suggestion form
console.log('Show help for case:', route.params?.caseId);
}}
accessibilityLabel="Help"
/>
),
})}
/>
{/* AI Prediction Filters Screen */}
<Stack.Screen
name="AIPredictionFilters"
component={ComingSoonScreen}
options={({ navigation }) => ({
title: 'Advanced Filters',
headerLeft: () => (
<HeaderBackButton onPress={() => navigation.goBack()} />
),
headerRight: () => (
<HeaderActionButton
iconName="refresh-cw"
onPress={() => {
// Reset filters
console.log('Reset filters');
}}
accessibilityLabel="Reset filters"
/>
),
})}
/>
{/* AI Prediction Stats Screen */}
<Stack.Screen
name="AIPredictionStats"
component={ComingSoonScreen}
options={({ navigation, route }) => ({
title: 'Statistics',
headerLeft: () => (
<HeaderBackButton onPress={() => navigation.goBack()} />
),
headerRight: () => (
<HeaderActionButton
iconName="download"
onPress={() => {
// Export statistics
console.log('Export stats:', route.params?.timeRange);
}}
accessibilityLabel="Export statistics"
/>
),
})}
/>
</Stack.Navigator>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
headerButton: {
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
marginHorizontal: theme.spacing.xs,
},
headerButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.primary,
fontWeight: theme.typography.fontWeight.medium,
},
});
export default AIPredictionStackNavigator;
/*
* End of File: AIPredictionStackNavigator.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,16 @@
/*
* File: index.ts
* Description: Navigation exports for AI Prediction module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export { default as AIPredictionStackNavigator } from './AIPredictionStackNavigator';
export * from './navigationTypes';
export * from './navigationUtils';
/*
* End of File: index.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,169 @@
/*
* File: navigationTypes.ts
* Description: Navigation type definitions for AI Prediction module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import type { StackNavigationProp } from '@react-navigation/stack';
import type { RouteProp } from '@react-navigation/native';
// ============================================================================
// AI PREDICTION STACK PARAM LIST
// ============================================================================
/**
* AI Prediction Stack Param List
*
* Purpose: Define navigation parameters for AI prediction screens
*
* Screens:
* - AIPredictionList: Main list of AI predictions
* - AIPredictionDetails: Detailed view of a specific prediction with suggestion form
* - AIPredictionFilters: Advanced filtering options
* - AIPredictionStats: Detailed statistics view
*/
export type AIPredictionStackParamList = {
AIPredictionList: undefined;
AIPredictionDetails: { caseId: string };
AIPredictionFilters: undefined;
AIPredictionStats: { timeRange?: 'today' | 'week' | 'month' };
};
// ============================================================================
// NAVIGATION PROP TYPES
// ============================================================================
/**
* AI Prediction List Navigation Prop
*
* Purpose: Navigation prop type for AI prediction list screen
*/
export type AIPredictionListNavigationProp = StackNavigationProp<
AIPredictionStackParamList,
'AIPredictionList'
>;
/**
* AI Prediction Details Navigation Prop
*
* Purpose: Navigation prop type for AI prediction details screen
*/
export type AIPredictionDetailsNavigationProp = StackNavigationProp<
AIPredictionStackParamList,
'AIPredictionDetails'
>;
/**
* AI Prediction Filters Navigation Prop
*
* Purpose: Navigation prop type for AI prediction filters screen
*/
export type AIPredictionFiltersNavigationProp = StackNavigationProp<
AIPredictionStackParamList,
'AIPredictionFilters'
>;
/**
* AI Prediction Stats Navigation Prop
*
* Purpose: Navigation prop type for AI prediction statistics screen
*/
export type AIPredictionStatsNavigationProp = StackNavigationProp<
AIPredictionStackParamList,
'AIPredictionStats'
>;
// ============================================================================
// ROUTE PROP TYPES
// ============================================================================
/**
* AI Prediction List Route Prop
*
* Purpose: Route prop type for AI prediction list screen
*/
export type AIPredictionListRouteProp = RouteProp<
AIPredictionStackParamList,
'AIPredictionList'
>;
/**
* AI Prediction Details Route Prop
*
* Purpose: Route prop type for AI prediction details screen
*/
export type AIPredictionDetailsRouteProp = RouteProp<
AIPredictionStackParamList,
'AIPredictionDetails'
>;
/**
* AI Prediction Filters Route Prop
*
* Purpose: Route prop type for AI prediction filters screen
*/
export type AIPredictionFiltersRouteProp = RouteProp<
AIPredictionStackParamList,
'AIPredictionFilters'
>;
/**
* AI Prediction Stats Route Prop
*
* Purpose: Route prop type for AI prediction statistics screen
*/
export type AIPredictionStatsRouteProp = RouteProp<
AIPredictionStackParamList,
'AIPredictionStats'
>;
// ============================================================================
// COMBINED PROP TYPES
// ============================================================================
/**
* AI Prediction List Screen Props
*
* Purpose: Combined props for AI prediction list screen
*/
export interface AIPredictionListScreenProps {
navigation: AIPredictionListNavigationProp;
route: AIPredictionListRouteProp;
}
/**
* AI Prediction Details Screen Props
*
* Purpose: Combined props for AI prediction details screen
*/
export interface AIPredictionDetailsScreenProps {
navigation: AIPredictionDetailsNavigationProp;
route: AIPredictionDetailsRouteProp;
}
/**
* AI Prediction Filters Screen Props
*
* Purpose: Combined props for AI prediction filters screen
*/
export interface AIPredictionFiltersScreenProps {
navigation: AIPredictionFiltersNavigationProp;
route: AIPredictionFiltersRouteProp;
}
/**
* AI Prediction Stats Screen Props
*
* Purpose: Combined props for AI prediction statistics screen
*/
export interface AIPredictionStatsScreenProps {
navigation: AIPredictionStatsNavigationProp;
route: AIPredictionStatsRouteProp;
}
/*
* End of File: navigationTypes.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,251 @@
/*
* File: navigationUtils.ts
* Description: Navigation utility functions for AI Prediction module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { CommonActions } from '@react-navigation/native';
import type { AIPredictionStackParamList } from './navigationTypes';
// ============================================================================
// NAVIGATION UTILITY FUNCTIONS
// ============================================================================
/**
* Navigate to AI Prediction Details
*
* Purpose: Navigate to AI prediction case details screen
*
* @param navigation - Navigation object
* @param caseId - AI prediction case ID
*/
export const navigateToAIPredictionDetails = (
navigation: any,
caseId: string
) => {
navigation.navigate('AIPredictionDetails', { caseId });
};
/**
* Navigate to AI Prediction Filters
*
* Purpose: Navigate to advanced filters screen
*
* @param navigation - Navigation object
*/
export const navigateToAIPredictionFilters = (navigation: any) => {
navigation.navigate('AIPredictionFilters');
};
/**
* Navigate to AI Prediction Statistics
*
* Purpose: Navigate to detailed statistics screen
*
* @param navigation - Navigation object
* @param timeRange - Optional time range filter
*/
export const navigateToAIPredictionStats = (
navigation: any,
timeRange?: 'today' | 'week' | 'month'
) => {
navigation.navigate('AIPredictionStats', { timeRange });
};
/**
* Go Back to AI Prediction List
*
* Purpose: Navigate back to AI prediction list screen
*
* @param navigation - Navigation object
*/
export const goBackToAIPredictionList = (navigation: any) => {
navigation.navigate('AIPredictionList');
};
/**
* Reset to AI Prediction List
*
* Purpose: Reset navigation stack to AI prediction list
*
* @param navigation - Navigation object
*/
export const resetToAIPredictionList = (navigation: any) => {
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'AIPredictionList' }],
})
);
};
/**
* Can Go Back
*
* Purpose: Check if navigation can go back
*
* @param navigation - Navigation object
* @returns Boolean indicating if can go back
*/
export const canGoBack = (navigation: any): boolean => {
return navigation.canGoBack();
};
/**
* Get Current Route Name
*
* Purpose: Get the current route name
*
* @param navigation - Navigation object
* @returns Current route name or undefined
*/
export const getCurrentRouteName = (navigation: any): string | undefined => {
return navigation.getCurrentRoute()?.name;
};
/**
* Get Current Route Params
*
* Purpose: Get the current route parameters
*
* @param navigation - Navigation object
* @returns Current route params or undefined
*/
export const getCurrentRouteParams = (navigation: any): any => {
return navigation.getCurrentRoute()?.params;
};
/**
* Navigate with Replace
*
* Purpose: Navigate to a screen by replacing the current one
*
* @param navigation - Navigation object
* @param routeName - Route name to navigate to
* @param params - Optional route parameters
*/
export const navigateWithReplace = (
navigation: any,
routeName: keyof AIPredictionStackParamList,
params?: any
) => {
navigation.replace(routeName, params);
};
/**
* Navigate with Push
*
* Purpose: Navigate to a screen by pushing it onto the stack
*
* @param navigation - Navigation object
* @param routeName - Route name to navigate to
* @param params - Optional route parameters
*/
export const navigateWithPush = (
navigation: any,
routeName: keyof AIPredictionStackParamList,
params?: any
) => {
navigation.push(routeName, params);
};
/**
* Pop Navigation Stack
*
* Purpose: Pop the specified number of screens from the stack
*
* @param navigation - Navigation object
* @param count - Number of screens to pop (default: 1)
*/
export const popNavigationStack = (navigation: any, count: number = 1) => {
navigation.pop(count);
};
/**
* Pop to Top
*
* Purpose: Pop to the top of the navigation stack
*
* @param navigation - Navigation object
*/
export const popToTop = (navigation: any) => {
navigation.popToTop();
};
/**
* Set Navigation Params
*
* Purpose: Set parameters for the current screen
*
* @param navigation - Navigation object
* @param params - Parameters to set
*/
export const setNavigationParams = (navigation: any, params: any) => {
navigation.setParams(params);
};
/**
* Add Navigation Listener
*
* Purpose: Add a navigation event listener
*
* @param navigation - Navigation object
* @param eventName - Event name to listen for
* @param callback - Callback function
* @returns Unsubscribe function
*/
export const addNavigationListener = (
navigation: any,
eventName: string,
callback: (e: any) => void
) => {
return navigation.addListener(eventName, callback);
};
/**
* Remove Navigation Listener
*
* Purpose: Remove a navigation event listener
*
* @param navigation - Navigation object
* @param eventName - Event name
* @param callback - Callback function
*/
export const removeNavigationListener = (
navigation: any,
eventName: string,
callback: (e: any) => void
) => {
navigation.removeListener(eventName, callback);
};
/**
* Check if Screen is Focused
*
* Purpose: Check if the current screen is focused
*
* @param navigation - Navigation object
* @returns Boolean indicating if screen is focused
*/
export const isScreenFocused = (navigation: any): boolean => {
return navigation.isFocused();
};
/**
* Get Navigation State
*
* Purpose: Get the current navigation state
*
* @param navigation - Navigation object
* @returns Navigation state
*/
export const getNavigationState = (navigation: any) => {
return navigation.getState();
};
/*
* End of File: navigationUtils.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,410 @@
/*
* File: aiPredictionSelectors.ts
* Description: Redux selectors for AI Prediction state
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '../../../store';
import { AIPredictionCase } from '../types';
// ============================================================================
// BASE SELECTORS
// ============================================================================
/**
* Select AI Prediction State
*
* Purpose: Get the entire AI prediction state
*/
export const selectAIPredictionState = (state: RootState) => state.aiPrediction;
/**
* Select Prediction Cases
*
* Purpose: Get all AI prediction cases
*/
export const selectPredictionCases = (state: RootState) => state.aiPrediction.predictionCases;
/**
* Select Current Case
*
* Purpose: Get the currently selected AI prediction case
*/
export const selectCurrentCase = (state: RootState) => state.aiPrediction.currentCase;
/**
* Select Loading State
*
* Purpose: Get the loading state for AI predictions
*/
export const selectIsLoading = (state: RootState) => state.aiPrediction.isLoading;
/**
* Select Loading Case Details State
*
* Purpose: Get the loading state for case details
*/
export const selectIsLoadingCaseDetails = (state: RootState) => state.aiPrediction.isLoadingCaseDetails;
/**
* Select Error
*
* Purpose: Get the current error message
*/
export const selectError = (state: RootState) => state.aiPrediction.error;
/**
* Select Search Query
*
* Purpose: Get the current search query
*/
export const selectSearchQuery = (state: RootState) => state.aiPrediction.searchQuery;
/**
* Select Filter States
*
* Purpose: Get all filter states
*/
export const selectUrgencyFilter = (state: RootState) => state.aiPrediction.selectedUrgencyFilter;
export const selectSeverityFilter = (state: RootState) => state.aiPrediction.selectedSeverityFilter;
export const selectCategoryFilter = (state: RootState) => state.aiPrediction.selectedCategoryFilter;
/**
* Select Sort Options
*
* Purpose: Get current sort configuration
*/
export const selectSortBy = (state: RootState) => state.aiPrediction.sortBy;
export const selectSortOrder = (state: RootState) => state.aiPrediction.sortOrder;
/**
* Select Pagination
*
* Purpose: Get pagination configuration
*/
export const selectCurrentPage = (state: RootState) => state.aiPrediction.currentPage;
export const selectItemsPerPage = (state: RootState) => state.aiPrediction.itemsPerPage;
export const selectTotalItems = (state: RootState) => state.aiPrediction.totalItems;
/**
* Select UI State
*
* Purpose: Get UI state flags
*/
export const selectShowFilters = (state: RootState) => state.aiPrediction.showFilters;
export const selectSelectedCaseIds = (state: RootState) => state.aiPrediction.selectedCaseIds;
// ============================================================================
// COMPUTED SELECTORS
// ============================================================================
/**
* Select Filtered and Sorted Cases
*
* Purpose: Get AI prediction cases filtered and sorted based on current settings
*/
export const selectFilteredAndSortedCases = createSelector(
[
selectPredictionCases,
selectSearchQuery,
selectUrgencyFilter,
selectSeverityFilter,
selectCategoryFilter,
selectSortBy,
selectSortOrder,
],
(cases, searchQuery, urgencyFilter, severityFilter, categoryFilter, sortBy, sortOrder) => {
let filteredCases = [...cases];
// Apply search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filteredCases = filteredCases.filter(case_ =>
case_.patid.toLowerCase().includes(query) ||
case_.prediction.label.toLowerCase().includes(query) ||
case_.prediction.anatomical_location.toLowerCase().includes(query)
);
}
// Apply urgency filter
if (urgencyFilter !== 'all') {
filteredCases = filteredCases.filter(case_ =>
case_.prediction.clinical_urgency === urgencyFilter
);
}
// Apply severity filter
if (severityFilter !== 'all') {
filteredCases = filteredCases.filter(case_ =>
case_.prediction.primary_severity === severityFilter
);
}
// Apply category filter
if (categoryFilter !== 'all') {
filteredCases = filteredCases.filter(case_ =>
case_.prediction.finding_category === categoryFilter
);
}
// Apply sorting
filteredCases.sort((a, b) => {
let comparison = 0;
switch (sortBy) {
case 'date':
comparison = new Date(a.created_at || '').getTime() - new Date(b.created_at || '').getTime();
break;
case 'urgency':
const urgencyOrder = { emergency: 5, urgent: 4, moderate: 3, low: 2, routine: 1 };
comparison = (urgencyOrder[a.prediction.clinical_urgency as keyof typeof urgencyOrder] || 0) -
(urgencyOrder[b.prediction.clinical_urgency as keyof typeof urgencyOrder] || 0);
break;
case 'confidence':
comparison = a.prediction.confidence_score - b.prediction.confidence_score;
break;
case 'severity':
const severityOrder = { high: 4, medium: 3, low: 2, none: 1 };
comparison = (severityOrder[a.prediction.primary_severity as keyof typeof severityOrder] || 0) -
(severityOrder[b.prediction.primary_severity as keyof typeof severityOrder] || 0);
break;
default:
break;
}
return sortOrder === 'desc' ? -comparison : comparison;
});
return filteredCases;
}
);
/**
* Select Paginated Cases
*
* Purpose: Get the current page of filtered and sorted cases
*/
export const selectPaginatedCases = createSelector(
[selectFilteredAndSortedCases, selectCurrentPage, selectItemsPerPage],
(filteredCases, currentPage, itemsPerPage) => {
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
return filteredCases.slice(startIndex, endIndex);
}
);
/**
* Select Critical Cases
*
* Purpose: Get cases marked as critical or emergency
*/
export const selectCriticalCases = createSelector(
[selectPredictionCases],
(cases) => cases.filter(case_ =>
case_.prediction.clinical_urgency === 'emergency' ||
case_.prediction.clinical_urgency === 'urgent' ||
case_.prediction.primary_severity === 'high' ||
case_.priority === 'critical'
)
);
/**
* Select Pending Cases
*
* Purpose: Get cases pending review
*/
export const selectPendingCases = createSelector(
[selectPredictionCases],
(cases) => cases.filter(case_ => case_.review_status === 'pending')
);
/**
* Select Reviewed Cases
*
* Purpose: Get cases that have been reviewed
*/
export const selectReviewedCases = createSelector(
[selectPredictionCases],
(cases) => cases.filter(case_ =>
case_.review_status === 'reviewed' ||
case_.review_status === 'confirmed' ||
case_.review_status === 'disputed'
)
);
/**
* Select Cases by Urgency
*
* Purpose: Group cases by urgency level
*/
export const selectCasesByUrgency = createSelector(
[selectPredictionCases],
(cases) => {
const grouped = {
emergency: [] as AIPredictionCase[],
urgent: [] as AIPredictionCase[],
moderate: [] as AIPredictionCase[],
low: [] as AIPredictionCase[],
routine: [] as AIPredictionCase[],
};
cases.forEach(case_ => {
const urgency = case_.prediction.clinical_urgency as keyof typeof grouped;
if (grouped[urgency]) {
grouped[urgency].push(case_);
}
});
return grouped;
}
);
/**
* Select Cases Statistics
*
* Purpose: Get statistical overview of cases
*/
export const selectCasesStatistics = createSelector(
[selectPredictionCases],
(cases) => {
const total = cases.length;
const critical = cases.filter(c =>
c.prediction.clinical_urgency === 'emergency' ||
c.prediction.clinical_urgency === 'urgent'
).length;
const pending = cases.filter(c => c.review_status === 'pending').length;
const reviewed = cases.filter(c =>
c.review_status === 'reviewed' ||
c.review_status === 'confirmed'
).length;
const averageConfidence = total > 0
? cases.reduce((sum, c) => sum + c.prediction.confidence_score, 0) / total
: 0;
return {
total,
critical,
pending,
reviewed,
averageConfidence: Math.round(averageConfidence * 1000) / 1000, // Round to 3 decimal places
reviewProgress: total > 0 ? Math.round((reviewed / total) * 100) : 0,
};
}
);
/**
* Select Filter Counts
*
* Purpose: Get counts for each filter option
*/
export const selectFilterCounts = createSelector(
[selectPredictionCases],
(cases) => {
const urgencyCounts = {
all: cases.length,
emergency: 0,
urgent: 0,
moderate: 0,
low: 0,
routine: 0,
};
const severityCounts = {
all: cases.length,
high: 0,
medium: 0,
low: 0,
none: 0,
};
const categoryCounts = {
all: cases.length,
normal: 0,
abnormal: 0,
critical: 0,
warning: 0,
unknown: 0,
};
cases.forEach(case_ => {
// Count urgency
const urgency = case_.prediction.clinical_urgency as keyof typeof urgencyCounts;
if (urgencyCounts[urgency] !== undefined) {
urgencyCounts[urgency]++;
}
// Count severity
const severity = case_.prediction.primary_severity as keyof typeof severityCounts;
if (severityCounts[severity] !== undefined) {
severityCounts[severity]++;
}
// Count category
const category = case_.prediction.finding_category as keyof typeof categoryCounts;
if (categoryCounts[category] !== undefined) {
categoryCounts[category]++;
}
});
return {
urgency: urgencyCounts,
severity: severityCounts,
category: categoryCounts,
};
}
);
/**
* Select Total Pages
*
* Purpose: Calculate total number of pages based on filtered results
*/
export const selectTotalPages = createSelector(
[selectFilteredAndSortedCases, selectItemsPerPage],
(filteredCases, itemsPerPage) => Math.ceil(filteredCases.length / itemsPerPage)
);
/**
* Select Has Previous Page
*
* Purpose: Check if there's a previous page available
*/
export const selectHasPreviousPage = createSelector(
[selectCurrentPage],
(currentPage) => currentPage > 1
);
/**
* Select Has Next Page
*
* Purpose: Check if there's a next page available
*/
export const selectHasNextPage = createSelector(
[selectCurrentPage, selectTotalPages],
(currentPage, totalPages) => currentPage < totalPages
);
/**
* Select Active Filters Count
*
* Purpose: Count how many filters are currently active
*/
export const selectActiveFiltersCount = createSelector(
[selectSearchQuery, selectUrgencyFilter, selectSeverityFilter, selectCategoryFilter],
(searchQuery, urgencyFilter, severityFilter, categoryFilter) => {
let count = 0;
if (searchQuery.trim()) count++;
if (urgencyFilter !== 'all') count++;
if (severityFilter !== 'all') count++;
if (categoryFilter !== 'all') count++;
return count;
}
);
/*
* End of File: aiPredictionSelectors.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,621 @@
/*
* File: aiPredictionSlice.ts
* Description: Redux slice for AI Prediction state management
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import {
AIPredictionCase,
AIPredictionState,
AIPredictionStats,
AIPredictionAPIResponse
} from '../types';
import { aiPredictionAPI } from '../services';
// ============================================================================
// ASYNC THUNKS
// ============================================================================
/**
* Fetch AI Predictions Async Thunk
*
* Purpose: Fetch AI prediction results from API
*
* @param token - Authentication token
* @param params - Optional query parameters for filtering
* @returns Promise with AI prediction data or error
*/
export const fetchAIPredictions = createAsyncThunk(
'aiPrediction/fetchAIPredictions',
async (payload: {
token: string;
params?: {
page?: number;
limit?: number;
urgency?: string;
severity?: string;
category?: string;
search?: string;
}
}, { rejectWithValue }) => {
try {
const response: any = await aiPredictionAPI.getAllPredictions(payload.token, payload.params);
console.log('AI predictions response:', response);
if (response.ok && response.data && response.data.success) {
// Add additional metadata to each case for UI purposes
const enhancedCases = response.data.data.map((aiCase: AIPredictionCase) => ({
...aiCase,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
review_status: 'pending' as const,
priority: getPriorityFromPrediction(aiCase.prediction)
}));
console.log('Enhanced AI prediction cases:', enhancedCases);
return {
cases: enhancedCases as AIPredictionCase[],
total: response.data.total || enhancedCases.length,
page: response.data.page || 1,
limit: response.data.limit || 20
};
} else {
// Fallback to mock data for development
const mockData = generateMockAIPredictions();
return {
cases: mockData,
total: mockData.length,
page: 1,
limit: 20
};
}
} catch (error: any) {
console.error('Fetch AI predictions error:', error);
return rejectWithValue(error.message || 'Failed to fetch AI predictions.');
}
}
);
/**
* Fetch AI Prediction Case Details Async Thunk
*
* Purpose: Fetch detailed information for a specific AI prediction case
*
* @param caseId - AI prediction case ID
* @param token - Authentication token
* @returns Promise with case details or error
*/
export const fetchAIPredictionDetails = createAsyncThunk(
'aiPrediction/fetchAIPredictionDetails',
async (payload: { caseId: string; token: string }, { rejectWithValue }) => {
try {
const response: any = await aiPredictionAPI.getCaseDetails(payload.caseId, payload.token);
if (response.ok && response.data) {
return response.data as AIPredictionCase;
} else {
// Fallback to mock data
const mockCase = generateMockAIPredictions().find(c => c.patid === payload.caseId);
if (mockCase) {
return mockCase;
}
throw new Error('Case not found');
}
} catch (error: any) {
console.error('Fetch AI prediction details error:', error);
return rejectWithValue(error.message || 'Failed to fetch case details.');
}
}
);
/**
* Update Case Review Async Thunk
*
* Purpose: Update review status of an AI prediction case
*
* @param caseId - Case ID to update
* @param reviewData - Review data
* @param token - Authentication token
* @returns Promise with updated case or error
*/
export const updateCaseReview = createAsyncThunk(
'aiPrediction/updateCaseReview',
async (payload: {
caseId: string;
reviewData: {
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
reviewed_by?: string;
review_notes?: string;
priority?: 'critical' | 'high' | 'medium' | 'low';
};
token: string;
}, { rejectWithValue }) => {
try {
const response: any = await aiPredictionAPI.updateCaseReview(
payload.caseId,
payload.reviewData,
payload.token
);
if (response.ok && response.data) {
return {
caseId: payload.caseId,
...payload.reviewData,
updated_at: new Date().toISOString()
};
} else {
throw new Error('Failed to update case review');
}
} catch (error: any) {
console.error('Update case review error:', error);
return rejectWithValue(error.message || 'Failed to update case review.');
}
}
);
/**
* Fetch AI Prediction Statistics Async Thunk
*
* Purpose: Fetch statistics for AI predictions dashboard
*
* @param token - Authentication token
* @param timeRange - Time range filter
* @returns Promise with statistics data or error
*/
export const fetchAIPredictionStats = createAsyncThunk(
'aiPrediction/fetchAIPredictionStats',
async (payload: { token: string; timeRange?: 'today' | 'week' | 'month' }, { rejectWithValue }) => {
try {
const response: any = await aiPredictionAPI.getPredictionStats(payload.token, payload.timeRange);
if (response.ok && response.data) {
return response.data as AIPredictionStats;
} else {
// Fallback to mock stats
return generateMockStats();
}
} catch (error: any) {
console.error('Fetch AI prediction stats error:', error);
return rejectWithValue(error.message || 'Failed to fetch statistics.');
}
}
);
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
/**
* Get Priority from AI Prediction
*
* Purpose: Determine case priority based on AI prediction results
*/
function getPriorityFromPrediction(prediction: any): 'critical' | 'high' | 'medium' | 'low' {
if (prediction.clinical_urgency === 'emergency' || prediction.primary_severity === 'high') {
return 'critical';
}
if (prediction.clinical_urgency === 'urgent' || prediction.primary_severity === 'medium') {
return 'high';
}
if (prediction.clinical_urgency === 'moderate' || prediction.primary_severity === 'low') {
return 'medium';
}
return 'low';
}
/**
* Generate Mock AI Predictions
*
* Purpose: Generate mock data for development and testing
*/
function generateMockAIPredictions(): AIPredictionCase[] {
return [
{
patid: "demogw05-08-2017",
hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2",
prediction: {
label: "midline shift",
finding_type: "pathology",
clinical_urgency: "urgent",
confidence_score: 0.996,
finding_category: "abnormal",
primary_severity: "high",
anatomical_location: "brain"
},
created_at: "2024-01-15T10:30:00Z",
updated_at: "2024-01-15T10:30:00Z",
review_status: "pending",
priority: "critical"
},
{
patid: "demo-patient-002",
hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2",
prediction: {
label: "normal brain",
finding_type: "no_pathology",
clinical_urgency: "routine",
confidence_score: 0.892,
finding_category: "normal",
primary_severity: "none",
anatomical_location: "not_applicable"
},
created_at: "2024-01-15T09:15:00Z",
updated_at: "2024-01-15T09:15:00Z",
review_status: "reviewed",
priority: "low"
},
{
patid: "demo-patient-003",
hospital_id: "eec24855-d8ae-4fad-8e54-af0480343dc2",
prediction: {
label: "hemorrhage",
finding_type: "pathology",
clinical_urgency: "emergency",
confidence_score: 0.945,
finding_category: "critical",
primary_severity: "high",
anatomical_location: "temporal lobe"
},
created_at: "2024-01-15T11:45:00Z",
updated_at: "2024-01-15T11:45:00Z",
review_status: "confirmed",
priority: "critical"
}
];
}
/**
* Generate Mock Statistics
*
* Purpose: Generate mock statistics for development
*/
function generateMockStats(): AIPredictionStats {
return {
totalCases: 156,
criticalCases: 23,
urgentCases: 45,
reviewedCases: 89,
pendingCases: 67,
averageConfidence: 0.887,
todaysCases: 12,
weeklyTrend: 15.4
};
}
// ============================================================================
// INITIAL STATE
// ============================================================================
/**
* Initial AI Prediction State
*
* Purpose: Define the initial state for AI predictions
*
* Features:
* - Prediction cases list and management
* - Current case details
* - Loading states for async operations
* - Error handling and messages
* - Search and filtering
* - Pagination support
* - Cache management
*/
const initialState: AIPredictionState = {
// Prediction data
predictionCases: [],
currentCase: null,
// Loading states
isLoading: false,
isRefreshing: false,
isLoadingCaseDetails: false,
// Error handling
error: null,
// Search and filtering
searchQuery: '',
selectedUrgencyFilter: 'all',
selectedSeverityFilter: 'all',
selectedCategoryFilter: 'all',
sortBy: 'date',
sortOrder: 'desc',
// Pagination
currentPage: 1,
itemsPerPage: 20,
totalItems: 0,
// Cache management
lastUpdated: null,
cacheExpiry: null,
// UI state
showFilters: false,
selectedCaseIds: [],
};
// ============================================================================
// AI PREDICTION SLICE
// ============================================================================
/**
* AI Prediction Slice
*
* Purpose: Redux slice for AI prediction state management
*
* Features:
* - AI prediction data management
* - Search and filtering
* - Case review management
* - Pagination
* - Caching
* - Error handling
* - Loading states
*/
const aiPredictionSlice = createSlice({
name: 'aiPrediction',
initialState,
reducers: {
/**
* Clear Error Action
*
* Purpose: Clear AI prediction errors
*/
clearError: (state) => {
state.error = null;
},
/**
* Set Search Query Action
*
* Purpose: Set search query for AI predictions
*/
setSearchQuery: (state, action: PayloadAction<string>) => {
state.searchQuery = action.payload;
state.currentPage = 1; // Reset to first page when searching
},
/**
* Set Urgency Filter Action
*
* Purpose: Set urgency filter for AI predictions
*/
setUrgencyFilter: (state, action: PayloadAction<AIPredictionState['selectedUrgencyFilter']>) => {
state.selectedUrgencyFilter = action.payload;
state.currentPage = 1; // Reset to first page when filtering
},
/**
* Set Severity Filter Action
*
* Purpose: Set severity filter for AI predictions
*/
setSeverityFilter: (state, action: PayloadAction<AIPredictionState['selectedSeverityFilter']>) => {
state.selectedSeverityFilter = action.payload;
state.currentPage = 1; // Reset to first page when filtering
},
/**
* Set Category Filter Action
*
* Purpose: Set category filter for AI predictions
*/
setCategoryFilter: (state, action: PayloadAction<AIPredictionState['selectedCategoryFilter']>) => {
state.selectedCategoryFilter = action.payload;
state.currentPage = 1; // Reset to first page when filtering
},
/**
* Set Sort Action
*
* Purpose: Set sort options for AI predictions
*/
setSort: (state, action: PayloadAction<{ by: 'date' | 'urgency' | 'confidence' | 'severity'; order: 'asc' | 'desc' }>) => {
state.sortBy = action.payload.by;
state.sortOrder = action.payload.order;
},
/**
* Set Current Page Action
*
* Purpose: Set current page for pagination
*/
setCurrentPage: (state, action: PayloadAction<number>) => {
state.currentPage = action.payload;
},
/**
* Set Items Per Page Action
*
* Purpose: Set items per page for pagination
*/
setItemsPerPage: (state, action: PayloadAction<number>) => {
state.itemsPerPage = action.payload;
state.currentPage = 1; // Reset to first page when changing items per page
},
/**
* Set Current Case Action
*
* Purpose: Set the currently selected AI prediction case
*/
setCurrentCase: (state, action: PayloadAction<AIPredictionCase | null>) => {
state.currentCase = action.payload;
},
/**
* Update Case in List Action
*
* Purpose: Update an AI prediction case in the list
*/
updateCaseInList: (state, action: PayloadAction<AIPredictionCase>) => {
const index = state.predictionCases.findIndex(case_ => case_.patid === action.payload.patid);
if (index !== -1) {
state.predictionCases[index] = action.payload;
}
// Update current case if it's the same case
if (state.currentCase && state.currentCase.patid === action.payload.patid) {
state.currentCase = action.payload;
}
},
/**
* Toggle Show Filters Action
*
* Purpose: Toggle the display of filter options
*/
toggleShowFilters: (state) => {
state.showFilters = !state.showFilters;
},
/**
* Clear All Filters Action
*
* Purpose: Reset all filters to default values
*/
clearAllFilters: (state) => {
state.searchQuery = '';
state.selectedUrgencyFilter = 'all';
state.selectedSeverityFilter = 'all';
state.selectedCategoryFilter = 'all';
state.currentPage = 1;
},
/**
* Select Case Action
*
* Purpose: Add/remove case from selected cases
*/
toggleCaseSelection: (state, action: PayloadAction<string>) => {
const caseId = action.payload;
const index = state.selectedCaseIds.indexOf(caseId);
if (index === -1) {
state.selectedCaseIds.push(caseId);
} else {
state.selectedCaseIds.splice(index, 1);
}
},
/**
* Clear Selected Cases Action
*
* Purpose: Clear all selected cases
*/
clearSelectedCases: (state) => {
state.selectedCaseIds = [];
},
/**
* Clear Cache Action
*
* Purpose: Clear AI prediction data cache
*/
clearCache: (state) => {
state.predictionCases = [];
state.currentCase = null;
state.lastUpdated = null;
state.cacheExpiry = null;
},
},
extraReducers: (builder) => {
// Fetch AI Predictions
builder
.addCase(fetchAIPredictions.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchAIPredictions.fulfilled, (state, action) => {
state.isLoading = false;
state.predictionCases = action.payload.cases;
state.totalItems = action.payload.total;
state.lastUpdated = new Date().toLocaleString();
state.cacheExpiry = new Date(Date.now() + 5 * 60 * 1000).toLocaleString(); // 5 minutes
state.error = null;
})
.addCase(fetchAIPredictions.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
// Fetch AI Prediction Details
builder
.addCase(fetchAIPredictionDetails.pending, (state) => {
state.isLoadingCaseDetails = true;
state.error = null;
})
.addCase(fetchAIPredictionDetails.fulfilled, (state, action) => {
state.isLoadingCaseDetails = false;
state.currentCase = action.payload;
state.error = null;
})
.addCase(fetchAIPredictionDetails.rejected, (state, action) => {
state.isLoadingCaseDetails = false;
state.error = action.payload as string;
});
// Update Case Review
builder
.addCase(updateCaseReview.fulfilled, (state, action) => {
// Update case in list
const index = state.predictionCases.findIndex(case_ => case_.patid === action.payload.caseId);
if (index !== -1) {
state.predictionCases[index] = {
...state.predictionCases[index],
review_status: action.payload.review_status,
reviewed_by: action.payload.reviewed_by,
priority: action.payload.priority,
updated_at: action.payload.updated_at
};
}
// Update current case if it's the same case
if (state.currentCase && state.currentCase.patid === action.payload.caseId) {
state.currentCase = {
...state.currentCase,
review_status: action.payload.review_status,
reviewed_by: action.payload.reviewed_by,
priority: action.payload.priority,
updated_at: action.payload.updated_at
};
}
})
.addCase(updateCaseReview.rejected, (state, action) => {
state.error = action.payload as string;
});
},
});
// ============================================================================
// EXPORTS
// ============================================================================
export const {
clearError,
setSearchQuery,
setUrgencyFilter,
setSeverityFilter,
setCategoryFilter,
setSort,
setCurrentPage,
setItemsPerPage,
setCurrentCase,
updateCaseInList,
toggleShowFilters,
clearAllFilters,
toggleCaseSelection,
clearSelectedCases,
clearCache,
} = aiPredictionSlice.actions;
export default aiPredictionSlice.reducer;
/*
* End of File: aiPredictionSlice.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,15 @@
/*
* File: index.ts
* Description: Redux exports for AI Prediction module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export * from './aiPredictionSlice';
export * from './aiPredictionSelectors';
/*
* End of File: index.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,749 @@
/*
* File: AIPredictionsScreen.tsx
* Description: Main AI Predictions screen with data rendering and management
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useEffect, useCallback, useState } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
RefreshControl,
TouchableOpacity,
Alert,
StatusBar,
SafeAreaView,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
// Import Redux actions and selectors
import {
fetchAIPredictions,
setSearchQuery,
setUrgencyFilter,
setSeverityFilter,
setCategoryFilter,
setCurrentPage,
clearAllFilters,
toggleShowFilters,
toggleCaseSelection,
clearSelectedCases,
updateCaseReview,
} from '../redux';
import {
selectPaginatedCases,
selectIsLoading,
selectError,
selectSearchQuery,
selectUrgencyFilter,
selectSeverityFilter,
selectCategoryFilter,
selectShowFilters,
selectSelectedCaseIds,
selectCasesStatistics,
selectFilterCounts,
selectActiveFiltersCount,
selectCurrentPage,
selectTotalPages,
selectHasNextPage,
selectHasPreviousPage,
} from '../redux';
// Import components
import {
AIPredictionCard,
SearchBar,
FilterTabs,
LoadingState,
EmptyState,
StatsOverview,
} from '../components';
// Import types
import type { AIPredictionCase } from '../types';
// Import auth selector
import { selectUser } from '../../Auth/redux/authSelectors';
// ============================================================================
// INTERFACES
// ============================================================================
interface AIPredictionsScreenProps {
navigation: any;
route?: any;
}
// ============================================================================
// AI PREDICTIONS SCREEN COMPONENT
// ============================================================================
/**
* AIPredictionsScreen Component
*
* Purpose: Main screen for displaying and managing AI prediction cases
*
* Features:
* - Comprehensive AI predictions list
* - Real-time search and filtering
* - Statistics overview dashboard
* - Bulk case selection and actions
* - Pull-to-refresh functionality
* - Pagination support
* - Review status management
* - Modern card-based design
* - Error handling and retry
* - Loading states and empty states
* - Accessibility support
*/
const AIPredictionsScreen: React.FC<AIPredictionsScreenProps> = ({ navigation }) => {
// ============================================================================
// REDUX STATE
// ============================================================================
const dispatch = useAppDispatch();
// Auth state
const user :any = useAppSelector(selectUser);
// AI Prediction state
const cases = useAppSelector(selectPaginatedCases);
const isLoading = useAppSelector(selectIsLoading);
const error = useAppSelector(selectError);
const searchQuery = useAppSelector(selectSearchQuery);
const urgencyFilter = useAppSelector(selectUrgencyFilter);
const severityFilter = useAppSelector(selectSeverityFilter);
const categoryFilter = useAppSelector(selectCategoryFilter);
const showFilters = useAppSelector(selectShowFilters);
const selectedCaseIds = useAppSelector(selectSelectedCaseIds);
const statistics = useAppSelector(selectCasesStatistics);
const filterCounts = useAppSelector(selectFilterCounts);
const activeFiltersCount = useAppSelector(selectActiveFiltersCount);
const currentPage = useAppSelector(selectCurrentPage);
const totalPages = useAppSelector(selectTotalPages);
const hasNextPage = useAppSelector(selectHasNextPage);
const hasPreviousPage = useAppSelector(selectHasPreviousPage);
// ============================================================================
// LOCAL STATE
// ============================================================================
const [refreshing, setRefreshing] = useState(false);
const [showStats, setShowStats] = useState(true);
// ============================================================================
// EFFECTS
// ============================================================================
/**
* Load AI Predictions on Mount
*
* Purpose: Fetch AI predictions when component mounts
*/
console.log('user ===>', user);
useEffect(() => {
if (user?.access_token) {
loadAIPredictions();
}
}, [user?.access_token]);
/**
* Load AI Predictions on Filter Change
*
* Purpose: Reload data when filters change
*/
useEffect(() => {
if (user?.access_token) {
loadAIPredictions();
}
}, [urgencyFilter, severityFilter, categoryFilter, searchQuery, currentPage]);
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Load AI Predictions
*
* Purpose: Fetch AI predictions from API
*/
const loadAIPredictions = useCallback(async () => {
if (!user?.access_token) return;
try {
const params = {
page: currentPage,
limit: 20,
...(urgencyFilter !== 'all' && { urgency: urgencyFilter }),
...(severityFilter !== 'all' && { severity: severityFilter }),
...(categoryFilter !== 'all' && { category: categoryFilter }),
...(searchQuery.trim() && { search: searchQuery.trim() }),
};
await dispatch(fetchAIPredictions({
token: user.access_token,
params,
})).unwrap();
} catch (error) {
console.error('Failed to load AI predictions:', error);
// Error is handled by Redux state
}
}, [dispatch, user?.access_token, currentPage, urgencyFilter, severityFilter, categoryFilter, searchQuery]);
/**
* Handle Refresh
*
* Purpose: Handle pull-to-refresh
*/
const handleRefresh = useCallback(async () => {
setRefreshing(true);
await loadAIPredictions();
setRefreshing(false);
}, [loadAIPredictions]);
/**
* Handle Search
*
* Purpose: Handle search query change
*/
const handleSearch = useCallback((query: string) => {
dispatch(setSearchQuery(query));
}, [dispatch]);
/**
* Handle Filter Changes
*
* Purpose: Handle filter option changes
*/
const handleUrgencyFilterChange = useCallback((filter: typeof urgencyFilter) => {
dispatch(setUrgencyFilter(filter));
}, [dispatch]);
const handleSeverityFilterChange = useCallback((filter: typeof severityFilter) => {
dispatch(setSeverityFilter(filter));
}, [dispatch]);
const handleCategoryFilterChange = useCallback((filter: typeof categoryFilter) => {
dispatch(setCategoryFilter(filter));
}, [dispatch]);
/**
* Handle Clear Filters
*
* Purpose: Clear all active filters
*/
const handleClearFilters = useCallback(() => {
dispatch(clearAllFilters());
}, [dispatch]);
/**
* Handle Toggle Filters
*
* Purpose: Toggle filter visibility
*/
const handleToggleFilters = useCallback(() => {
dispatch(toggleShowFilters());
}, [dispatch]);
/**
* Handle Case Press
*
* Purpose: Navigate to case details
*/
const handleCasePress = useCallback((predictionCase: AIPredictionCase) => {
navigation.navigate('AIPredictionDetails', { caseId: predictionCase.patid });
}, [navigation]);
/**
* Handle Case Review
*
* Purpose: Handle case review action
*/
const handleCaseReview = useCallback(async (caseId: string) => {
if (!user?.access_token) return;
try {
await dispatch(updateCaseReview({
caseId,
reviewData: {
review_status: 'reviewed',
reviewed_by: user.name || user.email || 'Current User',
},
token: user.access_token,
})).unwrap();
Alert.alert(
'Review Updated',
'Case has been marked as reviewed.',
[{ text: 'OK' }]
);
} catch (error) {
Alert.alert(
'Error',
'Failed to update case review. Please try again.',
[{ text: 'OK' }]
);
}
}, [dispatch, user]);
/**
* Handle Case Selection
*
* Purpose: Handle case selection for bulk operations
*/
const handleCaseSelection = useCallback((caseId: string) => {
dispatch(toggleCaseSelection(caseId));
}, [dispatch]);
/**
* Handle Bulk Actions
*
* Purpose: Handle bulk actions on selected cases
*/
const handleBulkReview = useCallback(() => {
if (selectedCaseIds.length === 0) return;
Alert.alert(
'Bulk Review',
`Mark ${selectedCaseIds.length} cases as reviewed?`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Confirm',
onPress: async () => {
// Implement bulk review logic here
// For now, just clear selections
dispatch(clearSelectedCases());
},
},
]
);
}, [selectedCaseIds, dispatch]);
/**
* Handle Page Change
*
* Purpose: Handle pagination
*/
const handlePreviousPage = useCallback(() => {
if (hasPreviousPage) {
dispatch(setCurrentPage(currentPage - 1));
}
}, [dispatch, currentPage, hasPreviousPage]);
const handleNextPage = useCallback(() => {
if (hasNextPage) {
dispatch(setCurrentPage(currentPage + 1));
}
}, [dispatch, currentPage, hasNextPage]);
/**
* Handle Stats Press
*
* Purpose: Handle statistics card press
*/
const handleStatsPress = useCallback((statType: string) => {
// Navigate to detailed statistics or apply relevant filters
switch (statType) {
case 'critical':
dispatch(setUrgencyFilter('emergency'));
break;
case 'urgent':
dispatch(setUrgencyFilter('urgent'));
break;
case 'pending':
// Filter for pending reviews
break;
default:
break;
}
}, [dispatch]);
/**
* Handle Retry
*
* Purpose: Handle retry after error
*/
const handleRetry = useCallback(() => {
loadAIPredictions();
}, [loadAIPredictions]);
// ============================================================================
// RENDER FUNCTIONS
// ============================================================================
/**
* Render AI Prediction Case
*
* Purpose: Render individual AI prediction case card
*/
const renderPredictionCase = useCallback(({ item }: { item: AIPredictionCase }) => (
<AIPredictionCard
predictionCase={item}
onPress={handleCasePress}
onReview={handleCaseReview}
isSelected={selectedCaseIds.includes(item.patid)}
onToggleSelect={handleCaseSelection}
showReviewButton={true}
/>
), [handleCasePress, handleCaseReview, selectedCaseIds, handleCaseSelection]);
/**
* Render List Header
*
* Purpose: Render search, filters, and statistics
*/
const renderListHeader = useCallback(() => (
<View>
{/* Statistics Overview */}
{showStats && (
<StatsOverview
stats={{
totalCases: statistics.total,
criticalCases: statistics.critical,
urgentCases: 0, // Would need to be calculated from urgency filter
reviewedCases: statistics.reviewed,
pendingCases: statistics.pending,
averageConfidence: statistics.averageConfidence,
todaysCases: 0, // Would need to be calculated from today's data
weeklyTrend: 12.5, // Mock data
}}
onStatsPress={handleStatsPress}
/>
)}
{/* Search Bar */}
<SearchBar
value={searchQuery}
onChangeText={handleSearch}
placeholder="Search by patient ID, finding, location..."
/>
{/* Filter Controls */}
<View style={styles.filterControls}>
<TouchableOpacity
style={[styles.filterToggle, showFilters && styles.filterToggleActive]}
onPress={handleToggleFilters}
accessibilityRole="button"
accessibilityLabel="Toggle filters"
>
<Icon name="filter" size={18} color={showFilters ? theme.colors.background : theme.colors.primary} />
<Text style={[styles.filterToggleText, showFilters && styles.filterToggleActiveText]}>
Filters
</Text>
{activeFiltersCount > 0 && (
<View style={styles.filterBadge}>
<Text style={styles.filterBadgeText}>{activeFiltersCount}</Text>
</View>
)}
</TouchableOpacity>
{selectedCaseIds.length > 0 && (
<TouchableOpacity
style={styles.bulkActionButton}
onPress={handleBulkReview}
accessibilityRole="button"
accessibilityLabel={`Bulk actions for ${selectedCaseIds.length} selected cases`}
>
<Icon name="check-circle" size={18} color={theme.colors.background} />
<Text style={styles.bulkActionText}>
Review {selectedCaseIds.length}
</Text>
</TouchableOpacity>
)}
</View>
{/* Filter Tabs */}
{showFilters && (
<FilterTabs
selectedUrgencyFilter={urgencyFilter}
selectedSeverityFilter={severityFilter}
selectedCategoryFilter={categoryFilter}
onUrgencyFilterChange={handleUrgencyFilterChange}
onSeverityFilterChange={handleSeverityFilterChange}
onCategoryFilterChange={handleCategoryFilterChange}
onClearFilters={handleClearFilters}
filterCounts={filterCounts}
activeFiltersCount={activeFiltersCount}
/>
)}
{/* Results Summary */}
<View style={styles.resultsSummary}>
<Text style={styles.resultsText}>
{statistics.total} predictions found
{activeFiltersCount > 0 && ` (${activeFiltersCount} filters applied)`}
</Text>
</View>
</View>
), [
showStats,
statistics,
handleStatsPress,
searchQuery,
handleSearch,
showFilters,
handleToggleFilters,
activeFiltersCount,
selectedCaseIds,
handleBulkReview,
urgencyFilter,
severityFilter,
categoryFilter,
handleUrgencyFilterChange,
handleSeverityFilterChange,
handleCategoryFilterChange,
handleClearFilters,
filterCounts,
]);
/**
* Render List Footer
*
* Purpose: Render pagination controls
*/
const renderListFooter = useCallback(() => {
if (totalPages <= 1) return null;
return (
<View style={styles.paginationContainer}>
<TouchableOpacity
style={[styles.paginationButton, !hasPreviousPage && styles.paginationButtonDisabled]}
onPress={handlePreviousPage}
disabled={!hasPreviousPage}
accessibilityRole="button"
accessibilityLabel="Previous page"
>
<Icon name="chevron-left" size={20} color={hasPreviousPage ? theme.colors.primary : theme.colors.textMuted} />
<Text style={[styles.paginationButtonText, !hasPreviousPage && styles.paginationButtonTextDisabled]}>
Previous
</Text>
</TouchableOpacity>
<Text style={styles.paginationInfo}>
Page {currentPage} of {totalPages}
</Text>
<TouchableOpacity
style={[styles.paginationButton, !hasNextPage && styles.paginationButtonDisabled]}
onPress={handleNextPage}
disabled={!hasNextPage}
accessibilityRole="button"
accessibilityLabel="Next page"
>
<Text style={[styles.paginationButtonText, !hasNextPage && styles.paginationButtonTextDisabled]}>
Next
</Text>
<Icon name="chevron-right" size={20} color={hasNextPage ? theme.colors.primary : theme.colors.textMuted} />
</TouchableOpacity>
</View>
);
}, [totalPages, currentPage, hasPreviousPage, hasNextPage, handlePreviousPage, handleNextPage]);
// ============================================================================
// RENDER
// ============================================================================
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
{/* Header */}
<View style={styles.header}>
<Text style={styles.headerTitle}>AI Predictions</Text>
<TouchableOpacity
style={styles.headerButton}
onPress={() => setShowStats(!showStats)}
accessibilityRole="button"
accessibilityLabel="Toggle statistics"
>
<Icon name={showStats ? 'eye-off' : 'eye'} size={20} color={theme.colors.primary} />
</TouchableOpacity>
</View>
{/* Content */}
{error ? (
<EmptyState
title="Error Loading Predictions"
message={error}
iconName="alert-circle"
actionText="Retry"
onAction={handleRetry}
/>
) : isLoading && cases.length === 0 ? (
<LoadingState message="Loading AI predictions..." />
) : cases.length === 0 ? (
<EmptyState
title="No AI Predictions Found"
message="There are no AI prediction cases matching your current filters."
iconName="brain"
actionText="Clear Filters"
onAction={activeFiltersCount > 0 ? handleClearFilters : handleRefresh}
/>
) : (
<FlatList
data={cases}
renderItem={renderPredictionCase}
keyExtractor={(item) => item.patid}
ListHeaderComponent={renderListHeader}
ListFooterComponent={renderListFooter}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
colors={[theme.colors.primary]}
tintColor={theme.colors.primary}
/>
}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.listContent}
accessibilityRole="list"
/>
)}
</SafeAreaView>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
backgroundColor: theme.colors.background,
},
headerTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontWeight: theme.typography.fontWeight.bold,
color: theme.colors.textPrimary,
},
headerButton: {
padding: theme.spacing.sm,
},
filterControls: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
},
filterToggle: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
borderWidth: 1,
borderColor: theme.colors.primary,
gap: theme.spacing.sm,
},
filterToggleActive: {
backgroundColor: theme.colors.primary,
},
filterToggleText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.primary,
fontWeight: theme.typography.fontWeight.medium,
},
filterToggleActiveText: {
color: theme.colors.background,
},
filterBadge: {
backgroundColor: theme.colors.error,
borderRadius: 10,
minWidth: 20,
height: 20,
justifyContent: 'center',
alignItems: 'center',
},
filterBadgeText: {
fontSize: theme.typography.fontSize.caption,
color: theme.colors.background,
fontWeight: theme.typography.fontWeight.bold,
},
bulkActionButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.primary,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
gap: theme.spacing.sm,
},
bulkActionText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.background,
fontWeight: theme.typography.fontWeight.medium,
},
resultsSummary: {
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
},
resultsText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
},
listContent: {
paddingBottom: theme.spacing.xl,
},
paginationContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.lg,
marginTop: theme.spacing.lg,
},
paginationButton: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
borderWidth: 1,
borderColor: theme.colors.primary,
gap: theme.spacing.xs,
},
paginationButtonDisabled: {
borderColor: theme.colors.textMuted,
opacity: 0.5,
},
paginationButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.primary,
fontWeight: theme.typography.fontWeight.medium,
},
paginationButtonTextDisabled: {
color: theme.colors.textMuted,
},
paginationInfo: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
fontWeight: theme.typography.fontWeight.medium,
},
});
export default AIPredictionsScreen;
/*
* End of File: AIPredictionsScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,15 @@
/*
* File: index.ts
* Description: Screens exports for AI Prediction module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export { default as AIPredictionsScreen } from './AIPredictionsScreen';
export { default as AIPredictionDetailScreen } from './AIPredictionDetailScreen';
/*
* End of File: index.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,337 @@
/*
* File: aiPredictionAPI.ts
* Description: API service for AI prediction operations using apisauce
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import { create } from 'apisauce';
import { API_CONFIG, buildHeaders } from '../../../shared/utils';
const api = create({
baseURL: API_CONFIG.BASE_URL
});
/**
* AI Prediction API Service
*
* Purpose: Handle all AI prediction-related API operations
*
* Features:
* - Get AI prediction results for all patients
* - Get individual case prediction details
* - Update case review status
* - Search and filter predictions
* - Get prediction statistics
*/
export const aiPredictionAPI = {
/**
* Get All AI Prediction Results
*
* Purpose: Fetch all AI prediction results from server
*
* @param token - Authentication token
* @param params - Optional query parameters for filtering
* @returns Promise with AI prediction cases data
*/
getAllPredictions: (token: string, params?: {
page?: number;
limit?: number;
urgency?: string;
severity?: string;
category?: string;
search?: string;
}) => {
const queryParams = params ? { ...params } : {};
return api.get('/api/ai-cases/all-prediction-results', queryParams, buildHeaders({ token }));
},
/**
* Get AI Prediction Case Details
*
* Purpose: Fetch detailed information for a specific AI prediction case
*
* @param caseId - AI prediction case ID (patid)
* @param token - Authentication token
* @returns Promise with detailed case prediction data
*/
getCaseDetails: (caseId: string, token: string) => {
return api.get(`/api/ai-cases/prediction-details/${caseId}`, {}, buildHeaders({ token }));
},
/**
* Update Case Review Status
*
* Purpose: Update the review status of an AI prediction case
*
* @param caseId - AI prediction case ID
* @param reviewData - Review status and notes
* @param token - Authentication token
* @returns Promise with updated case data
*/
updateCaseReview: (caseId: string, reviewData: {
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
reviewed_by?: string;
review_notes?: string;
priority?: 'critical' | 'high' | 'medium' | 'low';
}, token: string) => {
return api.put(`/api/ai-cases/review/${caseId}`, reviewData, buildHeaders({ token }));
},
/**
* Get Prediction Statistics
*
* Purpose: Fetch AI prediction statistics for dashboard
*
* @param token - Authentication token
* @param timeRange - Optional time range filter (today, week, month)
* @returns Promise with prediction statistics
*/
getPredictionStats: (token: string, timeRange?: 'today' | 'week' | 'month') => {
const params = timeRange ? { timeRange } : {};
return api.get('/api/ai-cases/statistics', params, buildHeaders({ token }));
},
/**
* Search AI Prediction Cases
*
* Purpose: Search AI prediction cases by various criteria
*
* @param query - Search query (patient ID, hospital, findings)
* @param token - Authentication token
* @param filters - Additional search filters
* @returns Promise with search results
*/
searchPredictions: (query: string, token: string, filters?: {
urgency?: string[];
severity?: string[];
category?: string[];
dateRange?: { start: string; end: string };
}) => {
const params = {
q: query,
...(filters && { filters: JSON.stringify(filters) })
};
return api.get('/api/ai-cases/search', params, buildHeaders({ token }));
},
/**
* Get Predictions by Hospital
*
* Purpose: Fetch AI predictions filtered by hospital
*
* @param hospitalId - Hospital UUID
* @param token - Authentication token
* @param params - Optional query parameters
* @returns Promise with hospital-specific predictions
*/
getPredictionsByHospital: (hospitalId: string, token: string, params?: {
page?: number;
limit?: number;
urgency?: string;
startDate?: string;
endDate?: string;
}) => {
const queryParams = params ? { ...params } : {};
return api.get(`/api/ai-cases/hospital/${hospitalId}/predictions`, queryParams, buildHeaders({ token }));
},
/**
* Get Critical Predictions
*
* Purpose: Fetch only critical and urgent AI predictions
*
* @param token - Authentication token
* @returns Promise with critical predictions data
*/
getCriticalPredictions: (token: string) => {
return api.get('/api/ai-cases/critical-predictions', {}, buildHeaders({ token }));
},
/**
* Bulk Update Case Reviews
*
* Purpose: Update multiple case reviews at once
*
* @param caseIds - Array of case IDs to update
* @param reviewData - Review data to apply to all cases
* @param token - Authentication token
* @returns Promise with bulk update results
*/
bulkUpdateReviews: (caseIds: string[], reviewData: {
review_status: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
reviewed_by?: string;
review_notes?: string;
}, token: string) => {
return api.put('/api/ai-cases/bulk-review', {
caseIds,
reviewData
}, buildHeaders({ token }));
},
/**
* Export Predictions Data
*
* Purpose: Export AI predictions data for reporting
*
* @param token - Authentication token
* @param filters - Export filters
* @param format - Export format (csv, xlsx, pdf)
* @returns Promise with export file data
*/
exportPredictions: (token: string, filters?: {
urgency?: string[];
severity?: string[];
dateRange?: { start: string; end: string };
hospitalId?: string;
}, format: 'csv' | 'xlsx' | 'pdf' = 'csv') => {
const params = {
format,
...(filters && { filters: JSON.stringify(filters) })
};
return api.get('/api/ai-cases/export', params, buildHeaders({ token }));
},
/**
* Get Prediction Trends
*
* Purpose: Fetch prediction trends data for analytics
*
* @param token - Authentication token
* @param period - Time period for trends (daily, weekly, monthly)
* @returns Promise with trends data
*/
getPredictionTrends: (token: string, period: 'daily' | 'weekly' | 'monthly' = 'weekly') => {
return api.get('/api/ai-cases/trends', { period }, buildHeaders({ token }));
},
/**
* Submit Feedback on Prediction
*
* Purpose: Submit physician feedback on AI prediction accuracy
*
* @param caseId - AI prediction case ID
* @param feedbackData - Feedback data
* @param token - Authentication token
* @returns Promise with feedback submission result
*/
submitPredictionFeedback: (caseId: string, feedbackData: {
accuracy_rating: 1 | 2 | 3 | 4 | 5;
is_accurate: boolean;
physician_diagnosis?: string;
feedback_notes?: string;
improvement_suggestions?: string;
}, token: string) => {
return api.post(`/api/ai-cases/feedback/${caseId}`, feedbackData, buildHeaders({ token }));
},
/**
* Submit AI Suggestion
*
* Purpose: Submit physician suggestions for AI findings
*
* @param suggestionData - Suggestion data including patient ID, type, title, text, etc.
* @param token - Authentication token
* @returns Promise with suggestion submission result
*/
submitAISuggestion: (suggestionData: {
patid: string;
suggestion_type: string;
suggestion_title: string;
suggestion_text: string;
confidence_score: number;
priority_level: string;
category: string;
related_findings: Record<string, string>;
evidence_sources: string[];
contraindications: string;
cost_estimate: number;
time_estimate: string;
expires_at: string | null;
tags: string[];
ai_model_version: string;
}, token: string) => {
return api.post('/api/ai-cases/suggestions', suggestionData, buildHeaders({ token }));
},
/**
* Get AI Suggestions
*
* Purpose: Fetch AI suggestions for a specific case or all suggestions
*
* @param token - Authentication token
* @param params - Optional query parameters
* @returns Promise with suggestions data
*/
getAISuggestions: (token: string, params?: {
caseId?: string;
patientId?: string;
suggestionType?: string;
priority?: string;
status?: string;
page?: number;
limit?: number;
}) => {
const queryParams = params ? { ...params } : {};
return api.get('/api/ai-cases/suggestions', queryParams, buildHeaders({ token }));
},
/**
* Update AI Suggestion
*
* Purpose: Update an existing AI suggestion
*
* @param suggestionId - Suggestion ID to update
* @param updateData - Updated suggestion data
* @param token - Authentication token
* @returns Promise with updated suggestion data
*/
updateAISuggestion: (suggestionId: string, updateData: {
suggestion_title?: string;
suggestion_text?: string;
priority_level?: string;
category?: string;
related_findings?: Record<string, string>;
evidence_sources?: string[];
contraindications?: string;
cost_estimate?: number;
time_estimate?: string;
expires_at?: string | null;
tags?: string[];
}, token: string) => {
return api.put(`/api/ai-cases/suggestions/${suggestionId}`, updateData, buildHeaders({ token }));
},
/**
* Delete AI Suggestion
*
* Purpose: Delete an AI suggestion
*
* @param suggestionId - Suggestion ID to delete
* @param token - Authentication token
* @returns Promise with deletion result
*/
deleteAISuggestion: (suggestionId: string, token: string) => {
return api.delete(`/api/ai-cases/suggestions/${suggestionId}`, {}, buildHeaders({ token }));
},
/**
* Get Suggestion Statistics
*
* Purpose: Fetch statistics about AI suggestions
*
* @param token - Authentication token
* @param timeRange - Optional time range filter
* @returns Promise with suggestion statistics
*/
getSuggestionStats: (token: string, timeRange?: 'today' | 'week' | 'month') => {
const params = timeRange ? { timeRange } : {};
return api.get('/api/ai-cases/suggestions/statistics', params, buildHeaders({ token }));
}
};
/*
* End of File: aiPredictionAPI.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

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

View File

@ -0,0 +1,221 @@
/*
* File: aiPrediction.ts
* Description: Type definitions for AI Prediction module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
// ============================================================================
// AI PREDICTION INTERFACES
// ============================================================================
/**
* AI Prediction Interface
*
* Purpose: Define the structure of AI prediction data from the API
*
* Based on API response structure:
* - label: Type of medical finding
* - finding_type: Category of the finding
* - clinical_urgency: Urgency level for medical response
* - confidence_score: AI confidence in the prediction (0-1)
* - finding_category: General category of the finding
* - primary_severity: Severity level of the condition
* - anatomical_location: Where the finding is located
*/
export interface AIPrediction {
label: string;
finding_type: 'no_pathology' | 'pathology' | 'abnormal' | 'normal' | 'unknown';
clinical_urgency: 'urgent' | 'moderate' | 'low' | 'routine' | 'emergency';
confidence_score: number; // 0.0 to 1.0
finding_category: 'normal' | 'abnormal' | 'critical' | 'warning' | 'unknown';
primary_severity: 'high' | 'medium' | 'low' | 'none';
anatomical_location: string; // 'not_applicable' | specific location
}
/**
* AI Prediction Case Interface
*
* Purpose: Complete AI prediction case data structure
*
* Features:
* - Patient identification
* - Hospital association
* - AI prediction results
* - Metadata for tracking and display
*/
export interface AIPredictionCase {
patid: string; // Patient ID
hospital_id: string; // Hospital UUID
prediction: AIPrediction;
// Additional metadata (will be added for UI purposes)
created_at?: string;
updated_at?: string;
reviewed_by?: string;
review_status?: 'pending' | 'reviewed' | 'confirmed' | 'disputed';
priority?: 'critical' | 'high' | 'medium' | 'low';
processed_at?: string;
}
/**
* AI Prediction API Response Interface
*
* Purpose: Define the structure of API response
*/
export interface AIPredictionAPIResponse {
success: boolean;
data: AIPredictionCase[];
message?: string;
total?: number;
page?: number;
limit?: number;
}
/**
* AI Prediction State Interface
*
* Purpose: Define Redux state structure for AI predictions
*
* Features:
* - Prediction cases management
* - Current selected case
* - Loading states for async operations
* - Error handling and messages
* - Search and filtering
* - Pagination support
* - Cache management
*/
export interface AIPredictionState {
// Prediction data
predictionCases: AIPredictionCase[];
currentCase: AIPredictionCase | null;
// Loading states
isLoading: boolean;
isRefreshing: boolean;
isLoadingCaseDetails: boolean;
// Error handling
error: string | null;
// Search and filtering
searchQuery: string;
selectedUrgencyFilter: 'all' | 'urgent' | 'moderate' | 'low' | 'routine' | 'emergency';
selectedSeverityFilter: 'all' | 'high' | 'medium' | 'low' | 'none';
selectedCategoryFilter: 'all' | 'normal' | 'abnormal' | 'critical' | 'warning' | 'unknown';
sortBy: 'date' | 'urgency' | 'confidence' | 'severity';
sortOrder: 'asc' | 'desc';
// Pagination
currentPage: number;
itemsPerPage: number;
totalItems: number;
// Cache management
lastUpdated: String | null;
cacheExpiry: String | null;
// UI state
showFilters: boolean;
selectedCaseIds: string[];
}
/**
* AI Prediction Filter Options
*
* Purpose: Define available filter options for the UI
*/
export interface AIPredictionFilters {
urgency: Array<{
label: string;
value: AIPredictionState['selectedUrgencyFilter'];
count?: number;
}>;
severity: Array<{
label: string;
value: AIPredictionState['selectedSeverityFilter'];
count?: number;
}>;
category: Array<{
label: string;
value: AIPredictionState['selectedCategoryFilter'];
count?: number;
}>;
}
/**
* AI Prediction Statistics Interface
*
* Purpose: Define statistics data for dashboard display
*/
export interface AIPredictionStats {
totalCases: number;
criticalCases: number;
urgentCases: number;
reviewedCases: number;
pendingCases: number;
averageConfidence: number;
todaysCases: number;
weeklyTrend: number; // percentage change from last week
}
/**
* AI Prediction Navigation Props
*
* Purpose: Type safety for navigation between AI prediction screens
*/
export type AIPredictionNavigationProps = {
AIPredictionList: undefined;
AIPredictionDetails: { caseId: string };
AIPredictionFilters: undefined;
AIPredictionStats: undefined;
};
// ============================================================================
// UTILITY TYPES
// ============================================================================
/**
* Prediction Urgency Colors
*
* Purpose: Map urgency levels to UI colors
*/
export const URGENCY_COLORS = {
emergency: '#F44336', // Red
urgent: '#FF5722', // Deep Orange
moderate: '#FF9800', // Orange
low: '#FFC107', // Amber
routine: '#4CAF50', // Green
} as const;
/**
* Prediction Severity Colors
*
* Purpose: Map severity levels to UI colors
*/
export const SEVERITY_COLORS = {
high: '#F44336', // Red
medium: '#FF9800', // Orange
low: '#FFC107', // Amber
none: '#4CAF50', // Green
} as const;
/**
* Finding Category Colors
*
* Purpose: Map finding categories to UI colors
*/
export const CATEGORY_COLORS = {
critical: '#F44336', // Red
abnormal: '#FF9800', // Orange
warning: '#FFC107', // Amber
normal: '#4CAF50', // Green
unknown: '#9E9E9E', // Gray
} as const;
/*
* End of File: aiPrediction.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,14 @@
/*
* File: index.ts
* Description: Type definitions exports for AI Prediction module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export * from './aiPrediction';
/*
* End of File: index.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,641 @@
/*
* File: DocumentUploadStep.tsx
* Description: Document upload step component for signup flow with image picker
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ScrollView,
Image,
Alert,
PermissionsAndroid,
Platform,
KeyboardAvoidingView,
} from 'react-native';
import {
launchImageLibrary,
launchCamera,
ImagePickerResponse,
MediaType,
} from 'react-native-image-picker';
import { theme } from '../../../../theme/theme';
import { DocumentUploadStepProps } from '../../types/signup';
import Icon from 'react-native-vector-icons/Feather';
import { showError, showSuccess } from '../../../../shared/utils/toast';
import { validateFileType, validateFileSize, formatFileSize } from '../../../../shared/utils/fileUpload';
// ============================================================================
// INTERFACES
// ============================================================================
/**
* ImageData Interface
*
* Purpose: Defines the structure for image data
*/
interface ImageData {
uri: string;
name: string;
type: string;
size?: number;
}
// ============================================================================
// DOCUMENT UPLOAD STEP COMPONENT
// ============================================================================
/**
* DocumentUploadStep Component
*
* Purpose: Fourth step of signup flow - document upload with image picker
*
* Features:
* - Camera and gallery image selection
* - Image preview with file details
* - Real-time file size and type display
* - Permission handling for camera
* - Modern UI with proper header alignment
* - Continue button with loading state
* - Back navigation
*/
const DocumentUploadStep: React.FC<DocumentUploadStepProps> = ({
onContinue,
onBack,
data,
isLoading,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const [selectedImage, setSelectedImage] = useState<ImageData | null>(
data.id_photo_url ? {
uri: data.id_photo_url,
name: 'uploaded_document.jpg',
type: 'image/jpeg',
} : null
);
// ============================================================================
// PERMISSION HANDLERS
// ============================================================================
/**
* Request Camera Permission
*
* Purpose: Request camera permission for Android devices
*
* @returns Promise<boolean> - Whether permission was granted
*/
const requestCameraPermission = async (): Promise<boolean> => {
if (Platform.OS === 'android') {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.CAMERA,
{
title: 'Camera Permission',
message: 'This app needs camera permission to capture images.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
);
return granted === PermissionsAndroid.RESULTS.GRANTED;
} catch (err) {
console.warn('Camera permission error:', err);
return false;
}
}
return true;
};
// ============================================================================
// IMAGE PICKER HANDLERS
// ============================================================================
/**
* Handle Image Picker Selection
*
* Purpose: Show options for camera or gallery selection
*/
const handleImagePicker = () => {
Alert.alert(
'Select Image',
'Choose how you want to upload your document',
[
{
text: 'Camera',
onPress: () => handleCameraCapture(),
},
{
text: 'Gallery',
onPress: () => handleGalleryPicker(),
},
{
text: 'Cancel',
style: 'cancel',
},
]
);
};
/**
* Handle Camera Capture
*
* Purpose: Launch camera to capture image
*/
const handleCameraCapture = async () => {
const hasPermission = await requestCameraPermission();
if (!hasPermission) {
showError('Permission Error', 'Camera permission is required to capture images.');
return;
}
const options = {
mediaType: 'photo' as MediaType,
maxWidth: 2000,
maxHeight: 2000,
includeBase64: false,
};
launchCamera(options, (response: ImagePickerResponse) => {
if (response.didCancel) {
return;
}
if (response.errorMessage) {
showError('Camera Error', response.errorMessage);
return;
}
if (response.assets && response.assets[0]) {
const asset = response.assets[0];
const imageData: ImageData = {
uri: asset.uri!,
name: asset.fileName || `document_${Date.now()}.jpg`,
type: asset.type || 'image/jpeg',
size: asset.fileSize,
};
// Validate file type and size
if (!validateFileType(imageData)) {
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
return;
}
if (!validateFileSize(imageData, 10)) {
showError('File Too Large', 'Please select an image under 10MB.');
return;
}
setSelectedImage(imageData);
showSuccess('Success', 'Document captured successfully!');
}
});
};
/**
* Handle Gallery Picker
*
* Purpose: Launch image library to select image
*/
const handleGalleryPicker = () => {
const options = {
mediaType: 'photo' as MediaType,
maxWidth: 2000,
maxHeight: 2000,
includeBase64: false,
};
launchImageLibrary(options, (response: ImagePickerResponse) => {
if (response.didCancel) {
return;
}
if (response.errorMessage) {
showError('Gallery Error', response.errorMessage);
return;
}
if (response.assets && response.assets[0]) {
const asset = response.assets[0];
const imageData: ImageData = {
uri: asset.uri!,
name: asset.fileName || `document_${Date.now()}.jpg`,
type: asset.type || 'image/jpeg',
size: asset.fileSize,
};
// Validate file type and size
if (!validateFileType(imageData)) {
showError('Invalid File Type', 'Please select a JPEG, JPG, or PNG image.');
return;
}
if (!validateFileSize(imageData, 10)) {
showError('File Too Large', 'Please select an image under 10MB.');
return;
}
setSelectedImage(imageData);
showSuccess('Success', 'Document selected from gallery!');
}
});
};
// ============================================================================
// UTILITY FUNCTIONS
// ============================================================================
/**
* Get File Type Display
*
* Purpose: Get display name for file type
*
* @param type - MIME type
* @returns Display name for file type
*/
const getFileTypeDisplay = (type: string): string => {
if (type.includes('jpeg') || type.includes('jpg')) return 'JPEG';
if (type.includes('png')) return 'PNG';
if (type.includes('gif')) return 'GIF';
if (type.includes('webp')) return 'WebP';
return 'Image';
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Continue
*
* Purpose: Proceed to next step with selected image
*/
const handleContinue = () => {
if (!selectedImage) {
showError('Validation Error', 'Please upload a document to continue.');
return;
}
onContinue(selectedImage.uri);
};
/**
* Handle Remove Image
*
* Purpose: Remove selected image
*/
const handleRemoveImage = () => {
setSelectedImage(null);
};
// ============================================================================
// RENDER
// ============================================================================
return (
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.title}>Upload Document</Text>
<Text style={styles.subtitle}>Step 4 of 5</Text>
</View>
<View style={styles.headerSpacer} />
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.sectionTitle}>Upload your ID document</Text>
<Text style={styles.description}>
Please upload a clear photo of your hospital-issued ID for verification.
</Text>
{/* Document Upload Area */}
<TouchableOpacity
style={styles.uploadContainer}
onPress={handleImagePicker}
disabled={isLoading}
>
<View style={styles.uploadContent}>
{selectedImage ? (
<View style={styles.imagePreviewContainer}>
<Image source={{ uri: selectedImage.uri }} style={styles.imagePreview} />
<TouchableOpacity
style={styles.imageOverlay}
onPress={handleRemoveImage}
>
<Icon name="x" size={20} color={theme.colors.background} />
</TouchableOpacity>
<Text style={styles.uploadedText}>Document Uploaded</Text>
<Text style={styles.fileName}>{selectedImage.name}</Text>
<View style={styles.fileDetails}>
<Text style={styles.fileType}>{getFileTypeDisplay(selectedImage.type)}</Text>
{selectedImage.size && (
<Text style={styles.fileSize}> {formatFileSize(selectedImage.size)}</Text>
)}
</View>
</View>
) : (
<>
<Icon name="image" size={48} color={theme.colors.textSecondary} />
<Text style={styles.uploadText}>Tap to upload document</Text>
<Text style={styles.uploadSubtext}>JPG, PNG supported</Text>
</>
)}
</View>
</TouchableOpacity>
{selectedImage && (
<TouchableOpacity
style={styles.changeButton}
onPress={handleImagePicker}
disabled={isLoading}
>
<Text style={styles.changeButtonText}>Change Document</Text>
</TouchableOpacity>
)}
{/* Continue Button */}
<TouchableOpacity
style={[
styles.continueButton,
(!selectedImage || isLoading) && styles.continueButtonDisabled,
]}
onPress={handleContinue}
disabled={!selectedImage || isLoading}
>
<Text style={[
styles.continueButtonText,
(!selectedImage || isLoading) && styles.continueButtonTextDisabled,
]}>
{isLoading ? 'Processing...' : 'Continue'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view
scrollView: {
flex: 1,
},
// Scroll content
scrollContent: {
flexGrow: 1,
paddingHorizontal: theme.spacing.sm,
},
// Header section
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: theme.spacing.xl,
paddingBottom: theme.spacing.lg,
marginBottom: theme.spacing.lg,
},
// Back button
backButton: {
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.backgroundAlt,
},
// Header content
headerContent: {
flex: 1,
alignItems: 'center',
},
// Header spacer
headerSpacer: {
width: 40,
},
// Title
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Subtitle
subtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Content section
content: {
flex: 1,
justifyContent: 'center',
paddingBottom: theme.spacing.xl,
},
// Section title
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Description
description: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xl,
},
// Upload container
uploadContainer: {
backgroundColor: theme.colors.backgroundAlt,
borderWidth: 2,
borderColor: theme.colors.border,
borderStyle: 'dashed',
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.xl,
marginBottom: theme.spacing.md,
minHeight: 200,
},
// Upload content
uploadContent: {
alignItems: 'center',
},
// Image preview container
imagePreviewContainer: {
alignItems: 'center',
width: '100%',
},
// Image preview
imagePreview: {
width: '100%',
height: 150,
borderRadius: theme.borderRadius.medium,
marginBottom: theme.spacing.sm,
resizeMode: 'contain',
},
// Image overlay (remove button)
imageOverlay: {
position: 'absolute',
top: 0,
right: 20,
backgroundColor: 'rgba(0, 0, 0, 0.4)',
borderRadius: 17,
width: 34,
height: 34,
justifyContent: 'center',
alignItems: 'center',
},
// Upload text
uploadText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginTop: theme.spacing.sm,
},
// Upload subtext
uploadSubtext: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginTop: theme.spacing.xs,
},
// Uploaded text
uploadedText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.success,
marginTop: theme.spacing.sm,
},
// File name
fileName: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginTop: theme.spacing.xs,
textAlign: 'center',
maxWidth: '80%',
},
// File details
fileDetails: {
flexDirection: 'row',
alignItems: 'center',
marginTop: theme.spacing.xs,
},
// File type
fileType: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
},
// File size
fileSize: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
},
// Change button
changeButton: {
alignSelf: 'center',
marginBottom: theme.spacing.xl,
},
// Change button text
changeButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.primary,
},
// Continue button
continueButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.lg,
...theme.shadows.primary,
},
// Continue button disabled
continueButtonDisabled: {
backgroundColor: theme.colors.border,
opacity: 0.6,
},
// Continue button text
continueButtonText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},
// Continue button text disabled
continueButtonTextDisabled: {
color: theme.colors.textMuted,
},
});
export default DocumentUploadStep;
/*
* End of File: DocumentUploadStep.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,231 @@
/*
* File: EmailAlreadyRegisteredModal.tsx
* Description: Modal for when email is already registered
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Modal,
Alert,
} from 'react-native';
import { theme } from '../../../../theme/theme';
// ============================================================================
// INTERFACES
// ============================================================================
interface EmailAlreadyRegisteredModalProps {
visible: boolean;
onClose: () => void;
onGoToLogin: () => void;
}
// ============================================================================
// EMAIL ALREADY REGISTERED MODAL COMPONENT
// ============================================================================
/**
* EmailAlreadyRegisteredModal Component
*
* Purpose: Modal shown when user tries to register with an existing email
*
* Features:
* - Informative message about existing email
* - Option to go to login
* - Option to close and try different email
*/
const EmailAlreadyRegisteredModal: React.FC<EmailAlreadyRegisteredModalProps> = ({
visible,
onClose,
onGoToLogin,
}) => {
// ============================================================================
// HANDLERS
// ============================================================================
/**
* Handle Go To Login
*
* Purpose: Navigate to login screen
*/
const handleGoToLogin = () => {
onClose();
onGoToLogin();
};
/**
* Handle Try Different Email
*
* Purpose: Close modal and allow user to try different email
*/
const handleTryDifferentEmail = () => {
onClose();
};
// ============================================================================
// RENDER
// ============================================================================
return (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={onClose}
>
<View style={styles.overlay}>
<View style={styles.modalContainer}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>Email Already Registered</Text>
<Text style={styles.subtitle}>
This email address is already associated with an account.
</Text>
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.message}>
It looks like you already have an account with us. You can either:
</Text>
<View style={styles.optionsContainer}>
<Text style={styles.optionText}>
Sign in to your existing account
</Text>
<Text style={styles.optionText}>
Try a different email address
</Text>
</View>
</View>
{/* Actions */}
<View style={styles.actions}>
<TouchableOpacity
style={styles.secondaryButton}
onPress={handleTryDifferentEmail}
>
<Text style={styles.secondaryButtonText}>Try Different Email</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.primaryButton}
onPress={handleGoToLogin}
>
<Text style={styles.primaryButtonText}>Go to Login</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: theme.spacing.lg,
},
modalContainer: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.large,
padding: theme.spacing.xl,
width: '100%',
maxWidth: 400,
shadowColor: '#000000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
},
header: {
alignItems: 'center',
marginBottom: theme.spacing.lg,
},
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
textAlign: 'center',
},
subtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
textAlign: 'center',
},
content: {
marginBottom: theme.spacing.xl,
},
message: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.md,
fontFamily: theme.typography.fontFamily.regular,
},
optionsContainer: {
marginLeft: theme.spacing.sm,
},
optionText: {
fontSize: theme.typography.fontSize.bodyMedium,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
fontFamily: theme.typography.fontFamily.regular,
},
actions: {
flexDirection: 'column', // Changed from 'row' to 'column'
gap: theme.spacing.md,
},
secondaryButton: {
backgroundColor: theme.colors.background,
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
},
secondaryButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
},
primaryButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
shadowColor: theme.colors.primary,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.3,
shadowRadius: 4,
elevation: 3,
},
primaryButtonText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},
});
export default EmailAlreadyRegisteredModal;
/*
* End of File: EmailAlreadyRegisteredModal.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,370 @@
/*
* File: EmailStep.tsx
* Description: Email step component for signup flow
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
} from 'react-native';
import { theme } from '../../../../theme/theme';
import { EmailStepProps } from '../../types/signup';
import Icon from 'react-native-vector-icons/Feather';
// ============================================================================
// EMAIL STEP COMPONENT
// ============================================================================
/**
* EmailStep Component
*
* Purpose: First step of signup flow - email validation
*
* Features:
* - Email input with validation
* - Real-time email format checking
* - Continue button with loading state
* - Back navigation
* - Modern header with proper alignment
*/
const EmailStep: React.FC<EmailStepProps> = ({
onContinue,
onBack,
data,
isLoading,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const [email, setEmail] = useState(data.email || '');
const [emailError, setEmailError] = useState('');
// ============================================================================
// VALIDATION FUNCTIONS
// ============================================================================
/**
* Validate Email Format
*
* Purpose: Check if email format is valid
*/
const validateEmailFormat = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
/**
* Handle Email Change
*
* Purpose: Update email and clear errors
*/
const handleEmailChange = (text: string) => {
setEmail(text);
setEmailError('');
};
/**
* Handle Continue
*
* Purpose: Validate email and proceed to next step
*/
const handleContinue = () => {
// Clear previous errors
setEmailError('');
// Validate email format
if (!email.trim()) {
setEmailError('Email is required');
return;
}
if (!validateEmailFormat(email.trim())) {
setEmailError('Please enter a valid email address');
return;
}
// Call parent handler
onContinue(email.trim());
};
// ============================================================================
// RENDER
// ============================================================================
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Icon
name="arrow-left"
size={24}
color={theme.colors.textPrimary}
/>
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.title}>Create Account</Text>
<Text style={styles.subtitle}>Step 1 of 5</Text>
</View>
<View style={styles.headerSpacer} />
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.sectionTitle}>Enter your email address</Text>
<Text style={styles.description}>
We'll use this email to create your account and send you important updates.
</Text>
{/* Email Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Email Address</Text>
<TextInput
style={[
styles.input,
emailError ? styles.inputError : null,
]}
placeholder="Enter your email address"
placeholderTextColor={theme.colors.textMuted}
value={email}
onChangeText={handleEmailChange}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
autoComplete="email"
textContentType="emailAddress"
editable={!isLoading}
/>
{emailError ? (
<Text style={styles.errorText}>{emailError}</Text>
) : null}
</View>
{/* Continue Button */}
<TouchableOpacity
style={[
styles.continueButton,
(!email.trim() || isLoading) ? styles.continueButtonDisabled : null,
]}
onPress={handleContinue}
disabled={!email.trim() || isLoading}
>
<Text style={[
styles.continueButtonText,
(!email.trim() || isLoading) ? styles.continueButtonTextDisabled : null,
]}>
{isLoading ? 'Validating...' : 'Continue'}
</Text>
</TouchableOpacity>
{/* Additional Info */}
<View style={styles.infoContainer}>
<Text style={styles.infoText}>
By continuing, you agree to our Terms of Service and Privacy Policy.
</Text>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view
scrollView: {
flex: 1,
},
// Scroll content
scrollContent: {
flexGrow: 1,
paddingHorizontal: theme.spacing.sm,
},
// Header section
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: theme.spacing.xl,
paddingBottom: theme.spacing.lg,
marginBottom: theme.spacing.lg,
},
// Back button
backButton: {
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.backgroundAlt,
},
// Header content
headerContent: {
flex: 1,
alignItems: 'center',
},
// Header spacer
headerSpacer: {
width: 40,
},
// Title
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Subtitle
subtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Content section
content: {
flex: 1,
justifyContent: 'center',
paddingBottom: theme.spacing.xl,
},
// Section title
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Description
description: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xl,
},
// Input container
inputContainer: {
marginBottom: theme.spacing.xl,
},
// Input label
inputLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Input field
input: {
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
backgroundColor: theme.colors.background,
},
// Input error state
inputError: {
borderColor: theme.colors.error,
},
// Error text
errorText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.error,
marginTop: theme.spacing.xs,
},
// Continue button
continueButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.lg,
...theme.shadows.primary,
},
// Continue button disabled
continueButtonDisabled: {
backgroundColor: theme.colors.border,
opacity: 0.6,
},
// Continue button text
continueButtonText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},
// Continue button text disabled
continueButtonTextDisabled: {
color: theme.colors.textMuted,
},
// Info container
infoContainer: {
alignItems: 'center',
},
// Info text
infoText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
textAlign: 'center',
},
});
export default EmailStep;
/*
* End of File: EmailStep.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,592 @@
/*
* File: HospitalSelectionStep.tsx
* Description: Hospital selection step component for signup flow
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState, useMemo } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ScrollView,
FlatList,
ActivityIndicator,
TextInput,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { theme } from '../../../../theme/theme';
import { HospitalSelectionStepProps, Hospital } from '../../types/signup';
import Icon from 'react-native-vector-icons/Feather';
// ============================================================================
// HOSPITAL SELECTION STEP COMPONENT
// ============================================================================
/**
* HospitalSelectionStep Component
*
* Purpose: Fifth step of signup flow - hospital selection
*
* Features:
* - Hospital list display from Redux state
* - Hospital selection with visual feedback
* - Search functionality for hospitals
* - Loading states and error handling
* - Continue button with validation (sticky bottom)
* - Back navigation with modern header
* - Scrollable hospital list
*/
const HospitalSelectionStep: React.FC<HospitalSelectionStepProps> = ({
onContinue,
onBack,
data,
isLoading,
hospitals,
hospitalLoading,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const [selectedHospitalId, setSelectedHospitalId] = useState(data.hospital_id || '');
const [searchQuery, setSearchQuery] = useState('');
// ============================================================================
// COMPUTED VALUES
// ============================================================================
/**
* Filtered Hospitals
*
* Purpose: Filter hospitals based on search query
*/
const filteredHospitals = useMemo(() => {
if (!hospitals) return [];
if (!searchQuery.trim()) return hospitals;
const query = searchQuery.toLowerCase();
return hospitals.filter(hospital =>
hospital.hospital_name?.toLowerCase().includes(query)
);
}, [hospitals, searchQuery]);
// ============================================================================
// HANDLERS
// ============================================================================
/**
* Handle Hospital Selection
*
* Purpose: Select a hospital
*/
const handleHospitalSelect = (hospitalId: string) => {
setSelectedHospitalId(hospitalId);
};
/**
* Handle Continue
*
* Purpose: Validate selection and proceed to next step
*/
const handleContinue = () => {
if (!selectedHospitalId) {
// Show error or alert
return;
}
onContinue(selectedHospitalId);
};
/**
* Handle Search Input Change
*
* Purpose: Update search query
*/
const handleSearchChange = (text: string) => {
setSearchQuery(text);
};
/**
* Clear Search
*
* Purpose: Clear search query
*/
const handleClearSearch = () => {
setSearchQuery('');
};
// ============================================================================
// RENDER FUNCTIONS
// ============================================================================
/**
* Render Hospital Item
*
* Purpose: Render individual hospital item
*/
const renderHospitalItem = ({ item }: { item: Hospital }) => {
const isSelected = selectedHospitalId === item.hospital_id;
return (
<TouchableOpacity
style={[
styles.hospitalItem,
isSelected && styles.hospitalItemSelected,
]}
onPress={() => handleHospitalSelect(item.hospital_id || '')}
disabled={isLoading}
>
<View style={styles.hospitalContent}>
<View style={styles.hospitalInfo}>
<Text style={[
styles.hospitalName,
isSelected && styles.hospitalNameSelected,
]}>
{item.hospital_name || 'Unknown Hospital'}
</Text>
</View>
{isSelected && (
<View style={styles.selectedIcon}>
<Icon name="check" size={20} color={theme.colors.background} />
</View>
)}
</View>
</TouchableOpacity>
);
};
/**
* Render Search Input
*
* Purpose: Render search input field
*/
const renderSearchInput = () => (
<View style={styles.searchContainer}>
<View style={styles.searchInputWrapper}>
<Icon name="search" size={20} color={theme.colors.textMuted} style={styles.searchIcon} />
<TextInput
style={styles.searchInput}
placeholder="Search hospitals..."
placeholderTextColor={theme.colors.textMuted}
value={searchQuery}
onChangeText={handleSearchChange}
autoCapitalize="none"
autoCorrect={false}
editable={!hospitalLoading}
/>
{searchQuery.length > 0 && (
<TouchableOpacity onPress={handleClearSearch} style={styles.clearButton}>
<Icon name="x" size={18} color={theme.colors.textMuted} />
</TouchableOpacity>
)}
</View>
</View>
);
/**
* Render Loading State
*
* Purpose: Show loading indicator while fetching hospitals
*/
const renderLoadingState = () => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={theme.colors.primary} />
<Text style={styles.loadingText}>Loading hospitals...</Text>
</View>
);
/**
* Render Empty State
*
* Purpose: Show message when no hospitals are available
*/
const renderEmptyState = () => (
<View style={styles.emptyContainer}>
<Icon name="building" size={48} color={theme.colors.textMuted} />
<Text style={styles.emptyTitle}>
{searchQuery ? 'No Hospitals Found' : 'No Hospitals Available'}
</Text>
<Text style={styles.emptyText}>
{searchQuery
? 'Try adjusting your search terms or browse all hospitals.'
: 'There are no hospitals available at the moment. Please try again later.'
}
</Text>
</View>
);
/**
* Render Search Results Info
*
* Purpose: Show search results count
*/
const renderSearchResultsInfo = () => {
if (!searchQuery.trim()) return null;
return (
<View style={styles.searchResultsInfo}>
<Text style={styles.searchResultsText}>
{filteredHospitals.length} hospital{filteredHospitals.length !== 1 ? 's' : ''} found
</Text>
</View>
);
};
// ============================================================================
// RENDER
// ============================================================================
return (
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.title}>Create Account</Text>
<Text style={styles.subtitle}>Step 5 of 5</Text>
</View>
<View style={styles.headerSpacer} />
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.sectionTitle}>Select Your Hospital</Text>
<Text style={styles.description}>
Choose the hospital where you work or will be practicing.
</Text>
{/* Search Input */}
{renderSearchInput()}
{/* Search Results Info */}
{renderSearchResultsInfo()}
{/* Hospital List */}
<View style={styles.hospitalListContainer}>
{hospitalLoading ? (
renderLoadingState()
) : filteredHospitals.length > 0 ? (
<FlatList
data={filteredHospitals}
renderItem={renderHospitalItem}
keyExtractor={(item) => item.hospital_id || ''}
showsVerticalScrollIndicator={true}
contentContainerStyle={styles.hospitalList}
keyboardShouldPersistTaps="handled"
/>
) : (
renderEmptyState()
)}
</View>
</View>
{/* Sticky Continue Button */}
<View style={styles.stickyButtonContainer}>
<TouchableOpacity
style={[
styles.continueButton,
(!selectedHospitalId || isLoading || hospitalLoading)
? styles.continueButtonDisabled
: null,
]}
onPress={handleContinue}
disabled={!selectedHospitalId || isLoading || hospitalLoading}
>
<Text style={[
styles.continueButtonText,
(!selectedHospitalId || isLoading || hospitalLoading)
? styles.continueButtonTextDisabled
: null,
]}>
{isLoading ? 'Creating Account...' : 'Create Account'}
</Text>
</TouchableOpacity>
</View>
</View>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Header section
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: theme.spacing.xl,
paddingBottom: theme.spacing.lg,
paddingHorizontal: theme.spacing.lg,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
// Back button
backButton: {
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.backgroundAlt,
},
// Header content
headerContent: {
flex: 1,
alignItems: 'center',
},
// Header spacer
headerSpacer: {
width: 40,
},
// Title
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Subtitle
subtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Content section
content: {
flex: 1,
paddingHorizontal: theme.spacing.lg,
paddingTop: theme.spacing.lg,
},
// Section title
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Description
description: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.lg,
},
// Search container
searchContainer: {
marginBottom: theme.spacing.lg,
},
// Search input wrapper
searchInputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.backgroundAlt,
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
},
// Search icon
searchIcon: {
marginRight: theme.spacing.sm,
},
// Search input
searchInput: {
flex: 1,
paddingVertical: theme.spacing.md,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
},
// Clear button
clearButton: {
padding: theme.spacing.xs,
marginLeft: theme.spacing.sm,
},
// Search results info
searchResultsInfo: {
marginBottom: theme.spacing.md,
},
// Search results text
searchResultsText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textSecondary,
},
// Hospital list container
hospitalListContainer: {
flex: 1,
},
// Hospital list
hospitalList: {
paddingBottom: theme.spacing.lg,
},
// Hospital item
hospitalItem: {
backgroundColor: theme.colors.background,
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginBottom: theme.spacing.sm,
},
// Hospital item selected
hospitalItemSelected: {
borderColor: theme.colors.primary,
backgroundColor: theme.colors.tertiary,
},
// Hospital content
hospitalContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
// Hospital info
hospitalInfo: {
flex: 1,
},
// Hospital name
hospitalName: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
},
// Hospital name selected
hospitalNameSelected: {
color: theme.colors.primary,
fontFamily: theme.typography.fontFamily.bold,
},
// Selected icon
selectedIcon: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: theme.colors.primary,
alignItems: 'center',
justifyContent: 'center',
},
// Loading container
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: theme.spacing.xxl,
},
// Loading text
loadingText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginTop: theme.spacing.md,
},
// Empty container
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: theme.spacing.xxl,
},
// Empty title
emptyTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginTop: theme.spacing.md,
marginBottom: theme.spacing.sm,
},
// Empty text
emptyText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
textAlign: 'center',
paddingHorizontal: theme.spacing.lg,
},
// Sticky button container
stickyButtonContainer: {
paddingHorizontal: theme.spacing.lg,
paddingVertical: theme.spacing.lg,
backgroundColor: theme.colors.background,
borderTopWidth: 1,
borderTopColor: theme.colors.border,
},
// Continue button
continueButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
...theme.shadows.primary,
},
// Continue button disabled
continueButtonDisabled: {
backgroundColor: theme.colors.border,
opacity: 0.6,
},
// Continue button text
continueButtonText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},
// Continue button text disabled
continueButtonTextDisabled: {
color: theme.colors.textMuted,
},
});
export default HospitalSelectionStep;
/*
* End of File: HospitalSelectionStep.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,399 @@
/*
* File: NameStep.tsx
* Description: Name step component for signup flow
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
} from 'react-native';
import { theme } from '../../../../theme/theme';
import { NameStepProps } from '../../types/signup';
import Icon from 'react-native-vector-icons/Feather';
// ============================================================================
// NAME STEP COMPONENT
// ============================================================================
/**
* NameStep Component
*
* Purpose: Third step of signup flow - personal information
*
* Features:
* - First name, last name, and username inputs
* - Real-time validation with error handling
* - Continue button with loading state
* - Back navigation with modern header
*/
const NameStep: React.FC<NameStepProps> = ({
onContinue,
onBack,
data,
isLoading,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const [firstName, setFirstName] = useState(data.first_name || '');
const [lastName, setLastName] = useState(data.last_name || '');
const [username, setUsername] = useState(data.username || '');
const [errors, setErrors] = useState({
firstName: '',
lastName: '',
username: '',
});
// ============================================================================
// VALIDATION FUNCTIONS
// ============================================================================
/**
* Validate Input Fields
*
* Purpose: Check if all fields are valid
*/
const validateFields = (): boolean => {
const newErrors = {
firstName: '',
lastName: '',
username: '',
};
if (!firstName.trim()) {
newErrors.firstName = 'First name is required';
}
if (!lastName.trim()) {
newErrors.lastName = 'Last name is required';
}
if (!username.trim()) {
newErrors.username = 'Username is required';
} else if (username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
}
setErrors(newErrors);
return !Object.values(newErrors).some(error => error !== '');
};
/**
* Handle Continue
*
* Purpose: Validate fields and proceed to next step
*/
const handleContinue = () => {
if (validateFields()) {
onContinue(firstName.trim(), lastName.trim(), username.trim());
}
};
// ============================================================================
// RENDER
// ============================================================================
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.title}>Create Account</Text>
<Text style={styles.subtitle}>Step 3 of 5</Text>
</View>
<View style={styles.headerSpacer} />
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.sectionTitle}>Tell us about yourself</Text>
<Text style={styles.description}>
Please provide your name and choose a username for your account.
</Text>
{/* First Name Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>First Name</Text>
<TextInput
style={[
styles.input,
errors.firstName ? styles.inputError : null,
]}
placeholder="Enter your first name"
placeholderTextColor={theme.colors.textMuted}
value={firstName}
onChangeText={(text) => {
setFirstName(text);
setErrors(prev => ({ ...prev, firstName: '' }));
}}
autoCapitalize="words"
autoCorrect={false}
editable={!isLoading}
/>
{errors.firstName ? (
<Text style={styles.errorText}>{errors.firstName}</Text>
) : null}
</View>
{/* Last Name Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Last Name</Text>
<TextInput
style={[
styles.input,
errors.lastName ? styles.inputError : null,
]}
placeholder="Enter your last name"
placeholderTextColor={theme.colors.textMuted}
value={lastName}
onChangeText={(text) => {
setLastName(text);
setErrors(prev => ({ ...prev, lastName: '' }));
}}
autoCapitalize="words"
autoCorrect={false}
editable={!isLoading}
/>
{errors.lastName ? (
<Text style={styles.errorText}>{errors.lastName}</Text>
) : null}
</View>
{/* Username Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Username</Text>
<TextInput
style={[
styles.input,
errors.username ? styles.inputError : null,
]}
placeholder="Choose a username"
placeholderTextColor={theme.colors.textMuted}
value={username}
onChangeText={(text) => {
setUsername(text);
setErrors(prev => ({ ...prev, username: '' }));
}}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
{errors.username ? (
<Text style={styles.errorText}>{errors.username}</Text>
) : null}
</View>
{/* Continue Button */}
<TouchableOpacity
style={[
styles.continueButton,
(!firstName.trim() || !lastName.trim() || !username.trim() || isLoading)
? styles.continueButtonDisabled
: null,
]}
onPress={handleContinue}
disabled={!firstName.trim() || !lastName.trim() || !username.trim() || isLoading}
>
<Text style={[
styles.continueButtonText,
(!firstName.trim() || !lastName.trim() || !username.trim() || isLoading)
? styles.continueButtonTextDisabled
: null,
]}>
{isLoading ? 'Validating...' : 'Continue'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view
scrollView: {
flex: 1,
},
// Scroll content
scrollContent: {
flexGrow: 1,
paddingHorizontal: theme.spacing.sm,
},
// Header section
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: theme.spacing.xl,
paddingBottom: theme.spacing.lg,
marginBottom: theme.spacing.lg,
},
// Back button
backButton: {
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.backgroundAlt,
},
// Header content
headerContent: {
flex: 1,
alignItems: 'center',
},
// Header spacer
headerSpacer: {
width: 40,
},
// Title
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Subtitle
subtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Content section
content: {
flex: 1,
justifyContent: 'center',
paddingBottom: theme.spacing.xl,
},
// Section title
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Description
description: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xl,
},
// Input container
inputContainer: {
marginBottom: theme.spacing.xl,
},
// Input label
inputLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Input field
input: {
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
backgroundColor: theme.colors.background,
},
// Input error state
inputError: {
borderColor: theme.colors.error,
},
// Error text
errorText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.error,
marginTop: theme.spacing.xs,
},
// Continue button
continueButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.lg,
...theme.shadows.primary,
},
// Continue button disabled
continueButtonDisabled: {
backgroundColor: theme.colors.border,
opacity: 0.6,
},
// Continue button text
continueButtonText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},
// Continue button text disabled
continueButtonTextDisabled: {
color: theme.colors.textMuted,
},
});
export default NameStep;
/*
* End of File: NameStep.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,628 @@
/*
* File: PasswordStep.tsx
* Description: Password step component for signup flow with comprehensive validation
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
ScrollView,
} from 'react-native';
import { theme } from '../../../../theme/theme';
import { PasswordStepProps } from '../../types/signup';
import Icon from 'react-native-vector-icons/Feather';
// ============================================================================
// INTERFACES
// ============================================================================
/**
* PasswordRule Interface
*
* Purpose: Defines the structure for password validation rules
*/
interface PasswordRule {
id: string;
label: string;
validator: (password: string) => boolean;
isValid: boolean;
}
// ============================================================================
// PASSWORD STEP COMPONENT
// ============================================================================
/**
* PasswordStep Component
*
* Purpose: Second step of signup flow - password creation with comprehensive validation
*
* Features:
* - Password input with visibility toggle
* - Comprehensive password validation rules
* - Real-time password strength checking
* - Visual feedback for each requirement
* - Continue button with loading state
* - Back navigation with modern header
*/
const PasswordStep: React.FC<PasswordStepProps> = ({
onContinue,
onBack,
data,
isLoading,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const [password, setPassword] = useState(data.password || '');
const [confirmPassword, setConfirmPassword] = useState('');
const [passwordError, setPasswordError] = useState('');
const [confirmPasswordError, setConfirmPasswordError] = useState('');
const [isPasswordVisible, setPasswordVisible] = useState(false);
const [isConfirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
// Password validation rules
const [passwordRules, setPasswordRules] = useState<PasswordRule[]>([
{
id: 'length',
label: 'At least 8 characters',
validator: (pwd: string) => pwd.length >= 8,
isValid: false,
},
{
id: 'uppercase',
label: 'One uppercase letter',
validator: (pwd: string) => /[A-Z]/.test(pwd),
isValid: false,
},
{
id: 'lowercase',
label: 'One lowercase letter',
validator: (pwd: string) => /[a-z]/.test(pwd),
isValid: false,
},
{
id: 'number',
label: 'One number',
validator: (pwd: string) => /\d/.test(pwd),
isValid: false,
},
{
id: 'special',
label: 'One special character',
validator: (pwd: string) => /[!@#$%^&*(),.?":{}|<>]/.test(pwd),
isValid: false,
},
{
id: 'match',
label: 'Passwords match',
validator: (pwd: string) => pwd === confirmPassword && confirmPassword.length > 0,
isValid: false,
},
]);
// ============================================================================
// EFFECTS
// ============================================================================
/**
* useEffect for password validation
*
* Purpose: Update password rules when password or confirm password changes
*/
useEffect(() => {
updatePasswordRules(password);
}, [password, confirmPassword]);
// ============================================================================
// VALIDATION FUNCTIONS
// ============================================================================
/**
* Update Password Rules
*
* Purpose: Update password validation rules based on current password and confirm password
*
* @param pwd - Current password value
*/
const updatePasswordRules = (pwd: string) => {
setPasswordRules(prevRules =>
prevRules.map(rule => {
if (rule.id === 'match') {
return {
...rule,
isValid: pwd === confirmPassword && confirmPassword.length > 0,
};
}
return {
...rule,
isValid: rule.validator(pwd),
};
})
);
};
/**
* Validate Password
*
* Purpose: Check if all password requirements are met
*
* @param pwd - Password to validate
* @returns boolean indicating if password meets all requirements
*/
const validatePassword = (pwd: string): boolean => {
return passwordRules.every(rule => rule.isValid);
};
/**
* Handle Password Change
*
* Purpose: Update password and clear errors
*/
const handlePasswordChange = (text: string) => {
setPassword(text);
setPasswordError('');
// Clear confirm password error if passwords now match
if (confirmPassword && text === confirmPassword) {
setConfirmPasswordError('');
}
// Update password rules to reflect the match status
updatePasswordRules(text);
};
/**
* Handle Confirm Password Change
*
* Purpose: Update confirm password and validate match
*/
const handleConfirmPasswordChange = (text: string) => {
setConfirmPassword(text);
setConfirmPasswordError('');
// Check if passwords match
if (text && text !== password) {
setConfirmPasswordError('Passwords do not match');
}
// Update password rules to reflect the match status
updatePasswordRules(password);
};
/**
* Handle Continue
*
* Purpose: Validate password and proceed to next step
*/
const handleContinue = () => {
// Clear previous errors
setPasswordError('');
setConfirmPasswordError('');
// Validate password
if (!password.trim()) {
setPasswordError('Password is required');
return;
}
if (!validatePassword(password.trim())) {
setPasswordError('Please meet all password requirements');
return;
}
// Validate confirm password
if (!confirmPassword.trim()) {
setConfirmPasswordError('Please confirm your password');
return;
}
if (password.trim() !== confirmPassword.trim()) {
setConfirmPasswordError('Passwords do not match');
return;
}
// Call parent handler
onContinue(password.trim());
};
/**
* Render Password Rule
*
* Purpose: Render individual password validation rule
*
* @param rule - Password rule to render
* @returns JSX element for the rule
*/
const renderPasswordRule = (rule: PasswordRule) => (
<View key={rule.id} style={styles.ruleContainer}>
<View style={[styles.checkbox, rule.isValid && styles.checkboxChecked]}>
{rule.isValid && (
<Icon name="check" size={12} color={theme.colors.background} />
)}
</View>
<Text style={[styles.ruleText, rule.isValid && styles.ruleTextValid]}>
{rule.label}
</Text>
</View>
);
// ============================================================================
// RENDER
// ============================================================================
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.title}>Create Account</Text>
<Text style={styles.subtitle}>Step 2 of 5</Text>
</View>
<View style={styles.headerSpacer} />
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.sectionTitle}>Create a strong password</Text>
<Text style={styles.description}>
Choose a password that meets all the security requirements below.
</Text>
{/* Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Password</Text>
<View style={[
styles.inputWrapper,
passwordError ? styles.inputWrapperError : null,
]}>
<TextInput
style={styles.input}
placeholder="Enter your password"
placeholderTextColor={theme.colors.textMuted}
value={password}
onChangeText={handlePasswordChange}
secureTextEntry={!isPasswordVisible}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<TouchableOpacity
onPress={() => setPasswordVisible(!isPasswordVisible)}
style={styles.eyeIcon}
disabled={isLoading}
>
<Icon
name={isPasswordVisible ? 'eye-off' : 'eye'}
size={22}
color={theme.colors.textSecondary}
/>
</TouchableOpacity>
</View>
{passwordError ? (
<Text style={styles.errorText}>{passwordError}</Text>
) : null}
</View>
{/* Confirm Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Confirm Password</Text>
<View style={[
styles.inputWrapper,
confirmPasswordError ? styles.inputWrapperError : null,
]}>
<TextInput
style={styles.input}
placeholder="Confirm your password"
placeholderTextColor={theme.colors.textMuted}
value={confirmPassword}
onChangeText={handleConfirmPasswordChange}
secureTextEntry={!isConfirmPasswordVisible}
autoCapitalize="none"
autoCorrect={false}
editable={!isLoading}
/>
<TouchableOpacity
onPress={() => setConfirmPasswordVisible(!isConfirmPasswordVisible)}
style={styles.eyeIcon}
disabled={isLoading}
>
<Icon
name={isConfirmPasswordVisible ? 'eye-off' : 'eye'}
size={22}
color={theme.colors.textSecondary}
/>
</TouchableOpacity>
</View>
{confirmPasswordError ? (
<Text style={styles.errorText}>{confirmPasswordError}</Text>
) : null}
</View>
{/* Password Requirements */}
<View style={styles.requirementsContainer}>
<Text style={styles.requirementsTitle}>Password Requirements:</Text>
<View style={styles.rulesGrid}>
{passwordRules.map(renderPasswordRule)}
</View>
</View>
{/* Continue Button */}
<TouchableOpacity
style={[
styles.continueButton,
(!password.trim() || !confirmPassword.trim() || isLoading || !validatePassword(password) || password !== confirmPassword) ? styles.continueButtonDisabled : null,
]}
onPress={handleContinue}
disabled={!password.trim() || !confirmPassword.trim() || isLoading || !validatePassword(password) || password !== confirmPassword}
>
<Text style={[
styles.continueButtonText,
(!password.trim() || !confirmPassword.trim() || isLoading || !validatePassword(password) || password !== confirmPassword) ? styles.continueButtonTextDisabled : null,
]}>
{isLoading ? 'Processing...' : 'Continue'}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view
scrollView: {
flex: 1,
},
// Scroll content
scrollContent: {
flexGrow: 1,
paddingHorizontal: theme.spacing.sm,
},
// Header section
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: theme.spacing.xl,
paddingBottom: theme.spacing.lg,
marginBottom: theme.spacing.lg,
},
// Back button
backButton: {
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.backgroundAlt,
},
// Header content
headerContent: {
flex: 1,
alignItems: 'center',
},
// Header spacer
headerSpacer: {
width: 40,
},
// Title
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Subtitle
subtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Content section
content: {
flex: 1,
justifyContent: 'center',
paddingBottom: theme.spacing.xl,
},
// Section title
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Description
description: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xl,
},
// Input container
inputContainer: {
marginBottom: theme.spacing.xl,
},
// Input label
inputLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Input wrapper
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.background,
},
// Input field
input: {
flex: 1,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
},
// Eye icon
eyeIcon: {
padding: theme.spacing.sm,
},
// Input wrapper error state
inputWrapperError: {
borderColor: theme.colors.error,
},
// Error text
errorText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.error,
marginTop: theme.spacing.xs,
},
// Requirements container
requirementsContainer: {
marginBottom: theme.spacing.xl,
padding: theme.spacing.md,
backgroundColor: theme.colors.backgroundAlt,
borderRadius: theme.borderRadius.medium,
},
// Requirements title
requirementsTitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Rules grid
rulesGrid: {
gap: theme.spacing.xs,
},
// Rule container
ruleContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: theme.spacing.xs,
},
// Checkbox
checkbox: {
width: 18,
height: 18,
borderRadius: 4,
borderWidth: 2,
borderColor: theme.colors.border,
backgroundColor: theme.colors.backgroundAlt,
marginRight: theme.spacing.sm,
alignItems: 'center',
justifyContent: 'center',
},
// Checkbox checked
checkboxChecked: {
backgroundColor: theme.colors.primary,
borderColor: theme.colors.primary,
},
// Rule text
ruleText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
flex: 1,
},
// Rule text valid
ruleTextValid: {
color: theme.colors.success,
},
// Continue button
continueButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.lg,
...theme.shadows.primary,
},
// Continue button disabled
continueButtonDisabled: {
backgroundColor: theme.colors.border,
opacity: 0.6,
},
// Continue button text
continueButtonText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},
// Continue button text disabled
continueButtonTextDisabled: {
color: theme.colors.textMuted,
},
});
export default PasswordStep;
/*
* End of File: PasswordStep.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

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