dashboard added and profile update

This commit is contained in:
yashwin-foxy 2025-08-14 20:16:03 +05:30
parent 692a8156da
commit 467dc0b8cf
34 changed files with 6046 additions and 1857 deletions

View File

@ -29,7 +29,7 @@ NeoScan_Physician/
│ │ │ │ ├── QuickActions.tsx # Emergency quick actions
│ │ │ │ └── DepartmentStats.tsx # Department statistics
│ │ │ ├── screens/ # Dashboard screens
│ │ │ │ └── ERDashboardScreen.tsx # Main ER dashboard
│ │ │ │ └── DashboardScreen.tsx # Main ER dashboard
│ │ │ ├── hooks/ # Dashboard custom hooks
│ │ │ ├── redux/ # Dashboard state management
│ │ │ ├── services/ # Dashboard API services
@ -223,7 +223,7 @@ NeoScan_Physician/
### Dashboard Module
**Purpose**: Main ER dashboard with patient monitoring and alerts
- **ERDashboardScreen**: Main dashboard with patient list and statistics
- **DashboardScreen**: Main dashboard with patient list and statistics
- **PatientCard**: Individual patient information display
- **CriticalAlerts**: High-priority alert notifications
- **QuickActions**: Emergency procedure shortcuts

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -152,7 +152,12 @@ const AIPredictionStackNavigator: React.FC = () => {
{/* 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'} />}
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: () => (

View File

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

View File

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

View File

@ -226,7 +226,7 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ navigation }) => {
setIsLoading(true);
try {
let role = 'er_physician';
let role = 'radiologist';
// Prepare form data with proper file handling
const formFields = {

View File

@ -38,7 +38,27 @@ export const authAPI = {
'Content-Type': 'multipart/form-data',
...(token && { 'Authorization': `Bearer ${token}` }),
},
})
}),
// Update user profile
updateUserProfile: (userId: string, profileData: {
first_name: string;
last_name: string;
}, token: string) => api.put(
`/api/auth/auth/admin/users/self/${userId}`,
profileData,
buildHeaders({ token })
),
// Change password (admin endpoint)
changePassword: (userId: string, passwordData: {
password: string;
}, token: string) => api.put(
`/api/auth/auth/admin/users/self/${userId}`,
passwordData,
buildHeaders({ token })
),
// Add more endpoints as needed
};

View File

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

View File

@ -6,7 +6,7 @@
*/
// Export screens
export { default as ERDashboardScreen } from './screens/ERDashboardScreen';
export { default as DashboardScreen } from './screens/DashboardScreen';
// Export navigation
export {
@ -14,7 +14,7 @@ export {
DashboardStackParamList,
DashboardNavigationProp,
DashboardScreenProps,
ERDashboardScreenProps,
DashboardScreenProps,
PatientDetailsScreenProps,
AlertDetailsScreenProps,
DepartmentStatsScreenProps,

View File

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

View File

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

View File

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

View File

@ -0,0 +1,811 @@
/*
* File: DashboardScreen.tsx
* Description: AI Analysis Dashboard - Main dashboard for AI predictions and analysis statistics
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
RefreshControl,
FlatList,
} from 'react-native';
import { theme } from '../../../theme/theme';
import { DashboardHeader } from '../components/DashboardHeader';
import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview';
/**
* DashboardScreenProps Interface
*
* Purpose: Defines the props required by the DashboardScreen component
*
* Props:
* - navigation: React Navigation object for screen navigation
*/
interface DashboardScreenProps {
navigation: any;
}
/**
* Dashboard Stats Data Interface
*
* Purpose: Defines the structure of the dashboard statistics data
*/
interface DashboardStats {
total_predictions: number;
total_patients: number;
total_feedbacks: number;
prediction_breakdown: Record<string, number>;
critical_findings: Record<string, number>;
midline_shift_stats: Record<string, number>;
hemorrhage_stats: Record<string, number>;
mass_lesion_stats: Record<string, number>;
edema_stats: Record<string, number>;
fracture_stats: Record<string, number>;
feedback_analysis: {
positive: number;
negative: number;
total: number;
};
hospital_distribution: Record<string, number>;
time_analysis: {
today: number;
this_week: number;
this_month: number;
this_year: number;
};
urgency_levels: {
critical: number;
urgent: number;
routine: number;
};
confidence_scores: {
high: number;
medium: number;
low: number;
};
feedback_rate_percentage: number;
predictions_with_feedback: number;
predictions_without_feedback: number;
average_feedback_per_prediction: string;
critical_case_percentage: number;
average_confidence_score: number;
}
/**
* Dashboard Summary Interface
*
* Purpose: Defines the structure of the dashboard summary data
*/
interface DashboardSummary {
total_cases: number;
critical_cases: number;
routine_cases: number;
feedback_coverage: string;
critical_case_rate: string;
average_confidence: string;
}
/**
* Complete Dashboard Data Interface
*
* Purpose: Defines the complete structure of the dashboard API response
*/
interface DashboardData {
success: boolean;
data: DashboardStats;
summary: DashboardSummary;
message: string;
}
/**
* DashboardScreen Component
*
* Purpose: AI Analysis Dashboard for physicians showing prediction statistics
*
* Dashboard Features:
* 1. AI prediction statistics and breakdown
* 2. Feedback analysis and coverage metrics
* 3. Confidence score distribution
* 4. Time-based analysis trends
* 5. Urgency level distribution
* 6. Pull-to-refresh functionality for live updates
*/
export const DashboardScreen: React.FC<DashboardScreenProps> = ({
navigation,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
// Refresh state for pull-to-refresh functionality
const [refreshing, setRefreshing] = useState(false);
// Dashboard data state
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
const [isLoading, setIsLoading] = useState(true);
// ============================================================================
// MOCK DATA GENERATION
// ============================================================================
/**
* generateMockDashboardData Function
*
* Purpose: Generate mock dashboard data based on the provided JSON structure
*
* Returns: DashboardData object with AI analysis statistics
*/
const generateMockDashboardData = (): DashboardData => ({
success: true,
data: {
total_predictions: 24,
total_patients: 9,
total_feedbacks: 6,
prediction_breakdown: {
"Other": 24
},
critical_findings: {},
midline_shift_stats: {},
hemorrhage_stats: {},
mass_lesion_stats: {},
edema_stats: {},
fracture_stats: {},
feedback_analysis: {
positive: 6,
negative: 0,
total: 6
},
hospital_distribution: {
"b491dfc2-521b-4eb1-8d88-02b0940ea1ff": 24
},
time_analysis: {
today: 24,
this_week: 24,
this_month: 24,
this_year: 24
},
urgency_levels: {
critical: 0,
urgent: 0,
routine: 24
},
confidence_scores: {
high: 23,
medium: 1,
low: 0
},
feedback_rate_percentage: 25,
predictions_with_feedback: 2,
predictions_without_feedback: 22,
average_feedback_per_prediction: "0.25",
critical_case_percentage: 0,
average_confidence_score: 0.89
},
summary: {
total_cases: 24,
critical_cases: 0,
routine_cases: 24,
feedback_coverage: "25.00%",
critical_case_rate: "0.00%",
average_confidence: "0.89"
},
message: "Statistics generated for 24 predictions"
});
// ============================================================================
// EFFECTS
// ============================================================================
/**
* useEffect for initial data loading
*
* Purpose: Load initial mock data when component mounts
*/
useEffect(() => {
const loadInitialData = async () => {
setIsLoading(true);
// Simulate API call delay
setTimeout(() => {}, 1000);
// Generate and set mock data
setDashboardData(generateMockDashboardData());
setIsLoading(false);
};
loadInitialData();
}, []);
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* handleRefresh Function
*
* Purpose: Handle pull-to-refresh functionality to update dashboard data
*/
const handleRefresh = async () => {
setRefreshing(true);
// Simulate API call with 1-second delay
setTimeout(() => {}, 1000);
// Update data with fresh mock data
setDashboardData(generateMockDashboardData());
setRefreshing(false);
};
// ============================================================================
// RENDER FUNCTIONS
// ============================================================================
/**
* renderStatsCard Function
*
* Purpose: Render individual statistics card component
*
* @param title - Card title
* @param value - Main value to display
* @param subtitle - Optional subtitle
* @param color - Optional color theme
* @returns Statistics card component
*/
const renderStatsCard = (title: string, value: string | number, subtitle?: string, color?: string) => (
<View style={[styles.statsCard, color && { borderLeftColor: color, borderLeftWidth: 4 }]}>
<Text style={styles.statsCardTitle}>{title}</Text>
<Text style={[styles.statsCardValue, color && { color }]}>{value}</Text>
{subtitle && <Text style={styles.statsCardSubtitle}>{subtitle}</Text>}
</View>
);
/**
* renderConfidenceBreakdown Function
*
* Purpose: Render confidence score breakdown section
*/
const renderConfidenceBreakdown = () => {
if (!dashboardData?.data.confidence_scores) return null;
const { high, medium, low } = dashboardData.data.confidence_scores;
const total = high + medium + low;
return (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Confidence Score Distribution</Text>
<View style={styles.confidenceContainer}>
<View style={styles.confidenceItem}>
<View style={[styles.confidenceBar, { backgroundColor: theme.colors.success, height: (high / total) * 100 }]} />
<Text style={styles.confidenceLabel}>High</Text>
<Text style={styles.confidenceValue}>{high}</Text>
</View>
<View style={styles.confidenceItem}>
<View style={[styles.confidenceBar, { backgroundColor: theme.colors.warning, height: (medium / total) * 100 }]} />
<Text style={styles.confidenceLabel}>Medium</Text>
<Text style={styles.confidenceValue}>{medium}</Text>
</View>
<View style={styles.confidenceItem}>
<View style={[styles.confidenceBar, { backgroundColor: theme.colors.error, height: (low / total) * 100 }]} />
<Text style={styles.confidenceLabel}>Low</Text>
<Text style={styles.confidenceValue}>{low}</Text>
</View>
</View>
</View>
);
};
/**
* renderUrgencyBreakdown Function
*
* Purpose: Render urgency level breakdown section
*/
const renderUrgencyBreakdown = () => {
if (!dashboardData?.data.urgency_levels) return null;
const { critical, urgent, routine } = dashboardData.data.urgency_levels;
return (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Case Urgency Distribution</Text>
<View style={styles.urgencyContainer}>
<View style={styles.urgencyItem}>
<View style={[styles.urgencyIndicator, { backgroundColor: theme.colors.error }]} />
<Text style={styles.urgencyLabel}>Critical</Text>
<Text style={styles.urgencyValue}>{critical}</Text>
</View>
<View style={styles.urgencyItem}>
<View style={[styles.urgencyIndicator, { backgroundColor: theme.colors.warning }]} />
<Text style={styles.urgencyLabel}>Urgent</Text>
<Text style={styles.urgencyValue}>{urgent}</Text>
</View>
<View style={styles.urgencyItem}>
<View style={[styles.urgencyIndicator, { backgroundColor: theme.colors.success }]} />
<Text style={styles.urgencyLabel}>Routine</Text>
<Text style={styles.urgencyValue}>{routine}</Text>
</View>
</View>
</View>
);
};
/**
* renderFeedbackAnalysis Function
*
* Purpose: Render feedback analysis section
*/
const renderFeedbackAnalysis = () => {
if (!dashboardData?.data.feedback_analysis) return null;
const { positive, negative, total } = dashboardData.data.feedback_analysis;
const positivePercentage = total > 0 ? ((positive / total) * 100).toFixed(1) : '0';
const negativePercentage = total > 0 ? ((negative / total) * 100).toFixed(1) : '0';
return (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Feedback Analysis</Text>
<View style={styles.feedbackContainer}>
<View style={styles.feedbackItem}>
<View style={[styles.feedbackIndicator, { backgroundColor: theme.colors.success }]} />
<Text style={styles.feedbackLabel}>Positive</Text>
<Text style={styles.feedbackValue}>{positive}</Text>
<Text style={styles.feedbackPercentage}>({positivePercentage}%)</Text>
</View>
<View style={styles.feedbackItem}>
<View style={[styles.feedbackIndicator, { backgroundColor: theme.colors.error }]} />
<Text style={styles.feedbackLabel}>Negative</Text>
<Text style={styles.feedbackValue}>{negative}</Text>
<Text style={styles.feedbackPercentage}>({negativePercentage}%)</Text>
</View>
</View>
<View style={styles.feedbackSummary}>
<Text style={styles.feedbackSummaryText}>
Feedback Coverage: {dashboardData.data.feedback_rate_percentage}%
</Text>
<Text style={styles.feedbackSummaryText}>
Average Feedback per Prediction: {dashboardData.data.average_feedback_per_prediction}
</Text>
</View>
</View>
);
};
/**
* renderTimeAnalysis Function
*
* Purpose: Render time-based analysis section
*/
const renderTimeAnalysis = () => {
if (!dashboardData?.data.time_analysis) return null;
const { today, this_week, this_month, this_year } = dashboardData.data.time_analysis;
return (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Time-based Analysis</Text>
<View style={styles.timeContainer}>
<View style={styles.timeItem}>
<Text style={styles.timeLabel}>Today</Text>
<Text style={styles.timeValue}>{today}</Text>
</View>
<View style={styles.timeItem}>
<Text style={styles.timeLabel}>This Week</Text>
<Text style={styles.timeValue}>{this_week}</Text>
</View>
<View style={styles.timeItem}>
<Text style={styles.timeLabel}>This Month</Text>
<Text style={styles.timeValue}>{this_month}</Text>
</View>
<View style={styles.timeItem}>
<Text style={styles.timeLabel}>This Year</Text>
<Text style={styles.timeValue}>{this_year}</Text>
</View>
</View>
</View>
);
};
/**
* renderHeader Function
*
* Purpose: Render the dashboard header section with key metrics
*/
const renderHeader = () => (
<View style={styles.header}>
{/* Dashboard header with title and refresh button */}
<View style={styles.headerTop}>
<Text style={styles.dashboardTitle}>AI Analysis Dashboard</Text>
<Text style={styles.dashboardSubtitle}>
{dashboardData?.message || 'Loading statistics...'}
</Text>
</View>
{/* Key statistics cards */}
<View style={styles.statsGrid}>
{renderStatsCard(
'Total Predictions',
dashboardData?.data.total_predictions || 0,
'AI analyses performed',
theme.colors.primary
)}
{renderStatsCard(
'Total Patients',
dashboardData?.data.total_patients || 0,
'Unique patients',
theme.colors.info
)}
{renderStatsCard(
'Feedback Rate',
`${dashboardData?.data.feedback_rate_percentage || 0}%`,
'User feedback coverage',
theme.colors.success
)}
{renderStatsCard(
'Avg Confidence',
(dashboardData?.data.average_confidence_score || 0).toFixed(2),
'AI prediction confidence',
theme.colors.warning
)}
</View>
</View>
);
// ============================================================================
// LOADING STATE
// ============================================================================
/**
* Loading state render
*
* Purpose: Show loading indicator while data is being generated
*/
if (isLoading) {
return (
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>Loading AI Analysis Dashboard...</Text>
</View>
);
}
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<View style={styles.container}>
{/* Scrollable dashboard content */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
colors={[theme.colors.primary]}
tintColor={theme.colors.primary}
/>
}
showsVerticalScrollIndicator={false}
>
{/* Dashboard header with key metrics */}
{renderHeader()}
{/* Confidence score breakdown */}
{renderConfidenceBreakdown()}
{/* Urgency level breakdown */}
{renderUrgencyBreakdown()}
{/* Feedback analysis */}
{renderFeedbackAnalysis()}
{/* Time-based analysis */}
{renderTimeAnalysis()}
{/* Bottom spacing for tab bar */}
<View style={styles.bottomSpacing} />
</ScrollView>
</View>
);
};
// ============================================================================
// STYLES SECTION
// ============================================================================
const styles = StyleSheet.create({
// Main container for the dashboard screen
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Loading container for initial data loading
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: theme.colors.background,
},
// Loading text styling
loadingText: {
fontSize: theme.typography.fontSize.bodyLarge,
color: theme.colors.textSecondary,
fontFamily: theme.typography.fontFamily.medium,
},
// Scroll view styling
scrollView: {
flex: 1,
},
// Scroll content styling
scrollContent: {
paddingBottom: theme.spacing.lg,
},
// Header section containing dashboard components
header: {
paddingHorizontal: theme.spacing.md,
paddingTop: theme.spacing.md,
},
// Header top section with title
headerTop: {
marginBottom: theme.spacing.lg,
},
// Dashboard title styling
dashboardTitle: {
fontSize: theme.typography.fontSize.displayLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Dashboard subtitle styling
dashboardSubtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Stats grid container
statsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: theme.spacing.sm,
marginBottom: theme.spacing.lg,
},
// Individual stats card styling
statsCard: {
flex: 1,
minWidth: '45%',
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
borderLeftWidth: 0,
borderLeftColor: 'transparent',
...theme.shadows.primary,
},
// Stats card title styling
statsCardTitle: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Stats card value styling
statsCardValue: {
fontSize: theme.typography.fontSize.displayMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Stats card subtitle styling
statsCardSubtitle: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
},
// Section container styling
section: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginHorizontal: theme.spacing.md,
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
// Section title styling
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.lg,
},
// Confidence breakdown container
confidenceContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'flex-end',
height: 120,
},
// Confidence item styling
confidenceItem: {
alignItems: 'center',
flex: 1,
},
// Confidence bar styling
confidenceBar: {
width: 40,
borderRadius: theme.borderRadius.small,
marginBottom: theme.spacing.sm,
minHeight: 4,
},
// Confidence label styling
confidenceLabel: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Confidence value styling
confidenceValue: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
},
// Urgency container styling
urgencyContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
},
// Urgency item styling
urgencyItem: {
alignItems: 'center',
flex: 1,
},
// Urgency indicator styling
urgencyIndicator: {
width: 16,
height: 16,
borderRadius: 8,
marginBottom: theme.spacing.xs,
},
// Urgency label styling
urgencyLabel: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Urgency value styling
urgencyValue: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
},
// Feedback container styling
feedbackContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
marginBottom: theme.spacing.md,
},
// Feedback item styling
feedbackItem: {
alignItems: 'center',
flex: 1,
},
// Feedback indicator styling
feedbackIndicator: {
width: 16,
height: 16,
borderRadius: 8,
marginBottom: theme.spacing.xs,
},
// Feedback label styling
feedbackLabel: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Feedback value styling
feedbackValue: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Feedback percentage styling
feedbackPercentage: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
},
// Feedback summary styling
feedbackSummary: {
borderTopWidth: 1,
borderTopColor: theme.colors.border,
paddingTop: theme.spacing.md,
},
// Feedback summary text styling
feedbackSummaryText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
textAlign: 'center',
marginBottom: theme.spacing.xs,
},
// Time container styling
timeContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
},
// Time item styling
timeItem: {
alignItems: 'center',
flex: 1,
},
// Time label styling
timeLabel: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Time value styling
timeValue: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
},
// Bottom spacing for tab bar
bottomSpacing: {
height: theme.spacing.xl,
},
});
/*
* End of File: DashboardScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -21,7 +21,6 @@ import { ERDashboard, Patient, Alert as AlertType } from '../../../shared/types'
import { PatientCard } from '../components/PatientCard';
import { CriticalAlerts } from '../components/CriticalAlerts';
import { DashboardHeader } from '../components/DashboardHeader';
import { QuickActions } from '../components/QuickActions';
import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview';
/**
@ -597,13 +596,6 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
/>
)}
{/* Quick action buttons for brain imaging tasks */}
<QuickActions
onQuickAction={(action) => {
console.log('Quick action:', action);
}}
/>
{/* Department statistics showing brain case distribution */}
{dashboard && <BrainPredictionsOverview dashboard={dashboard} />}

View File

@ -0,0 +1,57 @@
{
"success": true,
"data": {
"total_predictions": 24,
"total_patients": 9,
"total_feedbacks": 6,
"prediction_breakdown": {
"Other": 24
},
"critical_findings": {},
"midline_shift_stats": {},
"hemorrhage_stats": {},
"mass_lesion_stats": {},
"edema_stats": {},
"fracture_stats": {},
"feedback_analysis": {
"positive": 6,
"negative": 0,
"total": 6
},
"hospital_distribution": {
"b491dfc2-521b-4eb1-8d88-02b0940ea1ff": 24
},
"time_analysis": {
"today": 24,
"this_week": 24,
"this_month": 24,
"this_year": 24
},
"urgency_levels": {
"critical": 0,
"urgent": 0,
"routine": 24
},
"confidence_scores": {
"high": 23,
"medium": 1,
"low": 0
},
"feedback_rate_percentage": 25,
"predictions_with_feedback": 2,
"predictions_without_feedback": 22,
"average_feedback_per_prediction": "0.25",
"critical_case_percentage": 0,
"average_confidence_score": 0.89
},
"summary": {
"total_cases": 24,
"critical_cases": 0,
"routine_cases": 24,
"feedback_coverage": "25.00%",
"critical_case_rate": "0.00%",
"average_confidence": "0.89"
}
},
"message": "Statistics generated for 24 predictions"
}

View File

@ -21,18 +21,18 @@ import Icon from 'react-native-vector-icons/Feather';
// ============================================================================
interface FilterTabsProps {
selectedFilter: 'all' | 'Critical' | 'Routine' | 'Emergency';
onFilterChange: (filter: 'all' | 'Critical' | 'Routine' | 'Emergency') => void;
selectedFilter: 'all' | 'processed' | 'pending' | 'error';
onFilterChange: (filter: 'all' | 'processed' | 'pending' | 'error') => void;
patientCounts: {
all: number;
Critical: number;
Routine: number;
Emergency: number;
processed: number;
pending: number;
error: number;
};
}
interface FilterTab {
id: 'all' | 'Critical' | 'Routine' | 'Emergency';
id: 'all' | 'processed' | 'pending' | 'error';
label: string;
icon: string;
color: string;
@ -49,7 +49,7 @@ interface FilterTab {
* Purpose: Provide filtering options for patient list
*
* Features:
* - Multiple filter options (All, Active, Critical, Discharged)
* - Multiple filter options (All, Processed, Pending, Error)
* - Patient count display for each filter
* - Visual indicators with icons and colors
* - Horizontal scrollable layout
@ -74,26 +74,26 @@ const FilterTabs: React.FC<FilterTabsProps> = ({
activeColor: theme.colors.primary,
},
{
id: 'Critical',
label: 'Critical',
icon: 'alert-triangle',
color: theme.colors.error,
activeColor: theme.colors.error,
},
{
id: 'Emergency',
label: 'Emergency',
icon: 'alert-circle',
color: '#FF8C00',
activeColor: '#FF8C00',
},
{
id: 'Routine',
label: 'Routine',
id: 'processed',
label: 'Processed',
icon: 'check-circle',
color: theme.colors.success,
activeColor: theme.colors.success,
},
{
id: 'pending',
label: 'Pending',
icon: 'clock',
color: theme.colors.warning,
activeColor: theme.colors.warning,
},
{
id: 'error',
label: 'Error',
icon: 'alert-triangle',
color: theme.colors.error,
activeColor: theme.colors.error,
},
];
// ============================================================================
@ -112,12 +112,12 @@ const FilterTabs: React.FC<FilterTabsProps> = ({
switch (filterId) {
case 'all':
return patientCounts.all;
case 'Critical':
return patientCounts.Critical;
case 'Emergency':
return patientCounts.Emergency;
case 'Routine':
return patientCounts.Routine;
case 'processed':
return patientCounts.processed;
case 'pending':
return patientCounts.pending;
case 'error':
return patientCounts.error;
default:
return 0;
}
@ -190,9 +190,9 @@ const FilterTabs: React.FC<FilterTabsProps> = ({
</View>
</View>
{/* Critical Indicator */}
{tab.id === 'Critical' && patientCount > 0 && (
<View style={styles.criticalIndicator}>
{/* Error Indicator */}
{tab.id === 'error' && patientCount > 0 && (
<View style={styles.errorIndicator}>
<View style={styles.pulseDot} />
</View>
)}
@ -300,8 +300,8 @@ const styles = StyleSheet.create({
color: theme.colors.background,
},
// Critical Indicator
criticalIndicator: {
// Error Indicator
errorIndicator: {
position: 'absolute',
top: 8,
right: 8,

View File

@ -14,14 +14,14 @@ import {
} from 'react-native';
import { theme } from '../../../theme/theme';
import Icon from 'react-native-vector-icons/Feather';
import { MedicalCase, PatientDetails, Series } from '../../../shared/types';
import { PatientData } from '../redux/patientCareSlice';
// ============================================================================
// INTERFACES
// ============================================================================
interface PatientCardProps {
patient: MedicalCase;
patient: PatientData;
onPress: () => void;
onEmergencyPress?: () => void;
}
@ -38,9 +38,9 @@ interface PatientCardProps {
* Features:
* - Patient basic information from DICOM data
* - Modality and institution information
* - Case type with color coding
* - Processing status with color coding
* - Series information
* - Time since created
* - Time since processed
* - Emergency alert for critical cases
* - Modern ER-focused design
*/
@ -54,56 +54,33 @@ const PatientCard: React.FC<PatientCardProps> = ({
// ============================================================================
/**
* Parse JSON strings safely
* Get Status Color Configuration
*
* Purpose: Handle JSON string or object parsing for patient data
* Purpose: Get color and icon based on processing status
*
* @param jsonString - JSON string or object
* @returns Parsed object or empty object
*/
const parseJsonSafely = (jsonString: string | object) => {
if (typeof jsonString === 'object') {
return jsonString;
}
if (typeof jsonString === 'string') {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('Failed to parse JSON:', error);
return {};
}
}
return {};
};
/**
* Get Case Type Color Configuration
*
* Purpose: Get color and icon based on case type
*
* @param type - Case type
* @param status - Processing status
* @returns Color configuration object
*/
const getCaseTypeConfig = (type: string) => {
switch (type) {
case 'Critical':
return {
color: theme.colors.error,
icon: 'alert-triangle',
bgColor: '#FFF5F5'
};
case 'Emergency':
return {
color: '#FF8C00',
icon: 'alert-circle',
bgColor: '#FFF8E1'
};
case 'Routine':
const getStatusConfig = (status: string) => {
switch (status.toLowerCase()) {
case 'processed':
return {
color: theme.colors.success,
icon: 'check-circle',
bgColor: '#F0FFF4'
};
case 'pending':
return {
color: theme.colors.warning,
icon: 'clock',
bgColor: '#FFF8E1'
};
case 'error':
return {
color: theme.colors.error,
icon: 'alert-triangle',
bgColor: '#FFF5F5'
};
default:
return {
color: theme.colors.primary,
@ -122,13 +99,15 @@ const PatientCard: React.FC<PatientCardProps> = ({
* @returns Color code
*/
const getModalityColor = (modality: string) => {
switch (modality) {
switch (modality.toUpperCase()) {
case 'CT':
return '#4A90E2';
case 'MR':
return '#7B68EE';
case 'DX':
return '#50C878';
case 'DICOM':
return '#FF6B6B';
default:
return theme.colors.textSecondary;
}
@ -152,28 +131,47 @@ const PatientCard: React.FC<PatientCardProps> = ({
});
};
/**
* Get Time Since Processed
*
* Purpose: Get human-readable time since last processed
*
* @param dateString - ISO date string
* @returns Formatted time string
*/
const getTimeSinceProcessed = (dateString: string) => {
const now = new Date();
const processed = new Date(dateString);
const diffInMinutes = Math.floor((now.getTime() - processed.getTime()) / (1000 * 60));
if (diffInMinutes < 1) return 'Just now';
if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`;
return `${Math.floor(diffInMinutes / 1440)}d ago`;
};
// ============================================================================
// DATA EXTRACTION
// ============================================================================
const patientDetails = parseJsonSafely(patient.patientdetails);
const patientData = patientDetails.patientdetails || patientDetails;
const series = parseJsonSafely(patientDetails.series);
const typeConfig = getCaseTypeConfig(patient.type);
const patientInfo = patient.patient_info;
const seriesCount = patient.series_summary.length;
const statusConfig = getStatusConfig(patientInfo.status);
const isCritical = patientInfo.report_status === 'Critical' || patientInfo.status === 'Error';
// ============================================================================
// RENDER HELPERS
// ============================================================================
/**
* Render Case Type Badge
* Render Status Badge
*
* Purpose: Render case type indicator badge
* Purpose: Render processing status indicator badge
*/
const renderTypeBadge = () => (
<View style={[styles.typeBadge, { backgroundColor: typeConfig.color }]}>
<Icon name={typeConfig.icon} size={12} color={theme.colors.background} />
<Text style={styles.typeText}>{patient.type}</Text>
const renderStatusBadge = () => (
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bgColor, borderColor: statusConfig.color }]}>
<Icon name={statusConfig.icon} size={12} color={statusConfig.color} />
<Text style={[styles.statusText, { color: statusConfig.color }]}>{patientInfo.status}</Text>
</View>
);
@ -183,7 +181,7 @@ const PatientCard: React.FC<PatientCardProps> = ({
* Purpose: Render emergency alert button for critical cases
*/
const renderEmergencyButton = () => {
if (patient.type !== 'Critical') {
if (!isCritical) {
return null;
}
@ -207,8 +205,8 @@ const PatientCard: React.FC<PatientCardProps> = ({
<TouchableOpacity
style={[
styles.container,
// patient.type === 'Critical' && styles.containerCritical,
{ borderLeftColor: typeConfig.color }
isCritical && styles.containerCritical,
{ borderLeftColor: statusConfig.color }
]}
onPress={onPress}
activeOpacity={0.7}
@ -217,14 +215,14 @@ const PatientCard: React.FC<PatientCardProps> = ({
<View style={styles.header}>
<View style={styles.headerLeft}>
<Text style={styles.patientName}>
{patientData.Name || 'Unknown Patient'}
{patientInfo.name || 'Unknown Patient'}
</Text>
<Text style={styles.patientInfo}>
ID: {patientData.PatID || 'N/A'} {patientData.PatAge || 'N/A'}y {patientData.PatSex || 'N/A'}
ID: {patient.patid} {patientInfo.age || 'N/A'}y {patientInfo.sex || 'N/A'}
</Text>
</View>
<View style={styles.headerRight}>
{renderTypeBadge()}
{renderStatusBadge()}
{renderEmergencyButton()}
</View>
</View>
@ -237,27 +235,27 @@ const PatientCard: React.FC<PatientCardProps> = ({
<Text style={[
styles.infoValue,
styles.modalityText,
{ color: getModalityColor(patientData.Modality) }
{ color: getModalityColor(patientInfo.modality) }
]}>
{patientData.Modality || 'N/A'}
{patientInfo.modality || 'N/A'}
</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Status</Text>
<Text style={styles.infoLabel}>Files</Text>
<Text style={[
styles.infoValue,
{ color: patientData.Status === 'Active' ? theme.colors.success : theme.colors.textSecondary }
{ color: theme.colors.primary }
]}>
{patientData.Status || 'Unknown'}
{patient.total_files_processed}
</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Report</Text>
<Text style={[
styles.infoValue,
{ color: patientData.ReportStatus === 'Completed' ? theme.colors.success : theme.colors.warning }
{ color: patientInfo.report_status === 'Available' ? theme.colors.success : theme.colors.warning }
]}>
{patientData.ReportStatus || 'Pending'}
{patientInfo.report_status || 'Pending'}
</Text>
</View>
</View>
@ -266,7 +264,7 @@ const PatientCard: React.FC<PatientCardProps> = ({
<View style={styles.institutionRow}>
<Icon name="home" size={14} color={theme.colors.textSecondary} />
<Text style={styles.institutionText}>
{patientData.InstName || 'Unknown Institution'}
{patientInfo.institution || 'Unknown Institution'}
</Text>
</View>
</View>
@ -278,17 +276,22 @@ const PatientCard: React.FC<PatientCardProps> = ({
<Text style={styles.seriesLabel}>Series Information</Text>
</View>
<Text style={styles.seriesText}>
{Array.isArray(series) ? series.length : 0} Series Available
{seriesCount} Series Available {patientInfo.frame_count} Total Frames
</Text>
</View>
{/* Footer */}
<View style={styles.footer}>
<Text style={styles.dateText}>
{formatDate(patient.created_at)}
</Text>
<View style={styles.footerLeft}>
<Text style={styles.dateText}>
{formatDate(patientInfo.date)}
</Text>
<Text style={styles.processedText}>
{getTimeSinceProcessed(patient.last_processed_at)}
</Text>
</View>
<View style={styles.footerRight}>
<Text style={styles.caseId}>Case #{patient.id}</Text>
<Text style={styles.caseId}>Case #{patient.patid}</Text>
<Icon name="chevron-right" size={16} color={theme.colors.textMuted} />
</View>
</View>
@ -350,19 +353,19 @@ const styles = StyleSheet.create({
fontFamily: theme.typography.fontFamily.regular,
},
// Type Badge
typeBadge: {
// Status Badge
statusBadge: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
marginRight: theme.spacing.xs,
borderWidth: 1,
},
typeText: {
statusText: {
fontSize: 10,
fontWeight: 'bold',
color: theme.colors.background,
marginLeft: 4,
textTransform: 'uppercase',
},
@ -459,11 +462,20 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center',
},
footerLeft: {
flex: 1,
},
dateText: {
fontSize: 12,
color: theme.colors.textMuted,
fontFamily: theme.typography.fontFamily.regular,
},
processedText: {
fontSize: 11,
color: theme.colors.textSecondary,
marginTop: 2,
fontFamily: theme.typography.fontFamily.regular,
},
footerRight: {
flexDirection: 'row',
alignItems: 'center',

View File

@ -49,13 +49,11 @@ export interface PatientsScreenParams {
*
* Parameters:
* - patientId: Required patient ID to display details
* - patientName: Required patient name for display
* - medicalCase: Required medical case data with full patient information
* - patientName: Optional patient name for display (will be fetched from API if not provided)
*/
export interface PatientDetailsScreenParams {
patientId: string;
patientName: string;
medicalCase: MedicalCase;
patientName?: string;
}
// ============================================================================

View File

@ -6,8 +6,8 @@
*/
import { createSelector } from '@reduxjs/toolkit';
import { RootState } from '../../../store/store';
import { MedicalCase } from '../../../shared/types';
import { RootState } from '../../../store';
import { PatientData } from './patientCareSlice';
// ============================================================================
// BASE SELECTORS
@ -120,75 +120,59 @@ export const selectLastUpdated = (state: RootState) => state.patientCare.lastUpd
export const selectFilteredPatients = createSelector(
[selectPatients, selectSearchQuery, selectSelectedFilter, selectSortBy, selectSortOrder],
(patients, searchQuery, selectedFilter, sortBy, sortOrder) => {
// Ensure patients is always an array
if (!patients || !Array.isArray(patients)) {
return [];
}
let filteredPatients = [...patients];
// Helper function to parse JSON strings safely
const parseJsonSafely = (jsonString: string | object) => {
if (typeof jsonString === 'object') {
return jsonString;
}
if (typeof jsonString === 'string') {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('Failed to parse JSON:', error);
return {};
}
}
return {};
};
// Apply filter
// Apply filter based on processing status
if (selectedFilter !== 'all') {
filteredPatients = filteredPatients.filter(
patient => patient.type === selectedFilter
);
filteredPatients = filteredPatients.filter((patient: PatientData) => {
const status = patient.patient_info.status.toLowerCase();
return status === selectedFilter;
});
}
// Apply search
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase().trim();
filteredPatients = filteredPatients.filter(patient => {
const patientDetails = parseJsonSafely(patient.patientdetails);
const patientData = patientDetails.patientdetails || patientDetails;
filteredPatients = filteredPatients.filter((patient: PatientData) => {
const patientInfo = patient.patient_info;
const name = (patientData.Name || '').toLowerCase();
const patId = (patientData.PatID || '').toLowerCase();
const instName = (patientData.InstName || '').toLowerCase();
const modality = (patientData.Modality || '').toLowerCase();
const name = (patientInfo.name || '').toLowerCase();
const patId = (patient.patid || '').toLowerCase();
const institution = (patientInfo.institution || '').toLowerCase();
const modality = (patientInfo.modality || '').toLowerCase();
return (
name.includes(query) ||
patId.includes(query) ||
instName.includes(query) ||
institution.includes(query) ||
modality.includes(query)
);
});
}
// Apply sorting
filteredPatients.sort((a, b) => {
const patientDetailsA = parseJsonSafely(a.patientdetails);
const patientDataA = patientDetailsA.patientdetails || patientDetailsA;
const patientDetailsB = parseJsonSafely(b.patientdetails);
const patientDataB = patientDetailsB.patientdetails || patientDetailsB;
filteredPatients.sort((a: PatientData, b: PatientData) => {
let aValue: any;
let bValue: any;
switch (sortBy) {
case 'name':
aValue = (patientDataA.Name || '').toLowerCase();
bValue = (patientDataB.Name || '').toLowerCase();
aValue = (a.patient_info.name || '').toLowerCase();
bValue = (b.patient_info.name || '').toLowerCase();
break;
case 'age':
aValue = parseInt(patientDataA.PatAge || '0');
bValue = parseInt(patientDataB.PatAge || '0');
case 'processed':
aValue = new Date(a.last_processed_at).getTime();
bValue = new Date(b.last_processed_at).getTime();
break;
case 'date':
default:
aValue = new Date(a.created_at).getTime();
bValue = new Date(b.created_at).getTime();
aValue = new Date(a.patient_info.date).getTime();
bValue = new Date(b.patient_info.date).getTime();
break;
}
@ -206,53 +190,68 @@ export const selectFilteredPatients = createSelector(
);
/**
* Select Critical Patients
* Select Processed Patients
*
* Purpose: Get patients with critical priority
* Purpose: Get patients with processed status
*/
export const selectCriticalPatients = createSelector(
export const selectProcessedPatients = createSelector(
[selectPatients],
(patients) => patients.filter(patient => patient.type === 'Critical')
(patients) => {
if (!patients || !Array.isArray(patients)) return [];
return patients.filter((patient: PatientData) =>
patient.patient_info.status.toLowerCase() === 'processed'
);
}
);
/**
* Select Active Patients
* Select Pending Patients
*
* Purpose: Get patients with active status
* Purpose: Get patients with pending status
*/
export const selectActivePatients = createSelector(
export const selectPendingPatients = createSelector(
[selectPatients],
(patients: MedicalCase[]) => patients.filter((patient: MedicalCase) => {
// Parse patient details to check status
const parseJsonSafely = (jsonString: string | object) => {
if (typeof jsonString === 'object') return jsonString;
if (typeof jsonString === 'string') {
try { return JSON.parse(jsonString); } catch { return {}; }
}
return {};
};
const patientDetails = parseJsonSafely(patient.patientdetails);
const patientData = patientDetails.patientdetails || patientDetails;
return patientData.Status === 'Active';
})
(patients) => {
if (!patients || !Array.isArray(patients)) return [];
return patients.filter((patient: PatientData) =>
patient.patient_info.status.toLowerCase() === 'pending'
);
}
);
/**
* Select Patients by Department
* Select Error Patients
*
* Purpose: Get patients grouped by department
* Purpose: Get patients with error status
*/
export const selectPatientsByDepartment = createSelector(
export const selectErrorPatients = createSelector(
[selectPatients],
(patients: MedicalCase[]) => {
const grouped: { [key: string]: MedicalCase[] } = {};
(patients) => {
if (!patients || !Array.isArray(patients)) return [];
return patients.filter((patient: PatientData) =>
patient.patient_info.status.toLowerCase() === 'error'
);
}
);
/**
* Select Patients by Modality
*
* Purpose: Get patients grouped by imaging modality
*/
export const selectPatientsByModality = createSelector(
[selectPatients],
(patients) => {
if (!patients || !Array.isArray(patients)) return {};
patients.forEach((patient: MedicalCase) => {
const dept = patient.type; // Use case type instead of department
if (!grouped[dept]) {
grouped[dept] = [];
const grouped: { [key: string]: PatientData[] } = {};
patients.forEach((patient: PatientData) => {
const modality = patient.patient_info.modality || 'Unknown';
if (!grouped[modality]) {
grouped[modality] = [];
}
grouped[dept].push(patient);
grouped[modality].push(patient);
});
return grouped;
@ -266,43 +265,55 @@ export const selectPatientsByDepartment = createSelector(
*/
export const selectPatientStats = createSelector(
[selectPatients],
(patients: MedicalCase[]) => {
(patients) => {
if (!patients || !Array.isArray(patients)) {
return {
total: 0,
processed: 0,
pending: 0,
error: 0,
averageAge: 0,
modalities: {},
totalFiles: 0,
processedPercentage: 0,
pendingPercentage: 0,
errorPercentage: 0,
};
}
const total = patients.length;
const critical = patients.filter((p: MedicalCase) => p.type === 'Critical').length;
const emergency = patients.filter((p: MedicalCase) => p.type === 'Emergency').length;
const routine = patients.filter((p: MedicalCase) => p.type === 'Routine').length;
const processed = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'processed').length;
const pending = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'pending').length;
const error = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'error').length;
// Parse patient details for age calculation
const parseJsonSafely = (jsonString: string | object) => {
if (typeof jsonString === 'object') return jsonString;
if (typeof jsonString === 'string') {
try { return JSON.parse(jsonString); } catch { return {}; }
}
return {};
};
const totalAge = patients.reduce((sum: number, patient: MedicalCase) => {
const patientDetails = parseJsonSafely(patient.patientdetails);
const patientData = patientDetails.patientdetails || patientDetails;
return sum + parseInt(patientData.PatAge || '0');
// Calculate average age
const totalAge = patients.reduce((sum: number, patient: PatientData) => {
const age = parseInt(patient.patient_info.age) || 0;
return sum + age;
}, 0);
const averageAge = total > 0 ? Math.round(totalAge / total) : 0;
// Case type distribution
const caseTypes: { [key: string]: number } = {};
patients.forEach((patient: MedicalCase) => {
caseTypes[patient.type] = (caseTypes[patient.type] || 0) + 1;
// Modality distribution
const modalities: { [key: string]: number } = {};
patients.forEach((patient: PatientData) => {
const modality = patient.patient_info.modality || 'Unknown';
modalities[modality] = (modalities[modality] || 0) + 1;
});
// Total files processed
const totalFiles = patients.reduce((sum: number, patient: PatientData) => sum + (patient.total_files_processed || 0), 0);
return {
total,
critical,
emergency,
routine,
processed,
pending,
error,
averageAge,
caseTypes,
criticalPercentage: total > 0 ? Math.round((critical / total) * 100) : 0,
emergencyPercentage: total > 0 ? Math.round((emergency / total) * 100) : 0,
modalities,
totalFiles,
processedPercentage: total > 0 ? Math.round((processed / total) * 100) : 0,
pendingPercentage: total > 0 ? Math.round((pending / total) * 100) : 0,
errorPercentage: total > 0 ? Math.round((error / total) * 100) : 0,
};
}
);
@ -317,7 +328,10 @@ export const selectPatientStats = createSelector(
export const selectPatientById = (patientId: string) =>
createSelector(
[selectPatients],
(patients) => patients.find(patient => patient.id === patientId)
(patients) => {
if (!patients || !Array.isArray(patients)) return undefined;
return patients.find((patient: PatientData) => patient.patid === patientId);
}
);
/**
@ -328,32 +342,20 @@ export const selectPatientById = (patientId: string) =>
export const selectPatientsNeedAttention = createSelector(
[selectPatients],
(patients) => {
return patients.filter(patient => {
// Critical patients always need attention
if (patient.priority === 'CRITICAL') return true;
if (!patients || !Array.isArray(patients)) return [];
return patients.filter((patient: PatientData) => {
// Error patients always need attention
if (patient.patient_info.status.toLowerCase() === 'error') return true;
// Check vital signs for abnormal values
const vitals = patient.vitalSigns;
// Patients with critical report status
if (patient.patient_info.report_status.toLowerCase() === 'critical') return true;
// Check blood pressure (hypertensive crisis)
if (vitals.bloodPressure.systolic > 180 || vitals.bloodPressure.diastolic > 120) {
return true;
}
// Patients with high frame count (complex cases)
if (patient.patient_info.frame_count > 100) return true;
// Check heart rate (too high or too low)
if (vitals.heartRate.value > 120 || vitals.heartRate.value < 50) {
return true;
}
// Check temperature (fever or hypothermia)
if (vitals.temperature.value > 38.5 || vitals.temperature.value < 35) {
return true;
}
// Check oxygen saturation (low)
if (vitals.oxygenSaturation.value < 90) {
return true;
}
// Patients with multiple series (complex cases)
if (patient.series_summary.length > 5) return true;
return false;
});
@ -367,7 +369,7 @@ export const selectPatientsNeedAttention = createSelector(
*/
export const selectHasPatientData = createSelector(
[selectPatients],
(patients) => patients.length > 0
(patients) => patients && Array.isArray(patients) && patients.length > 0
);
/**
@ -378,7 +380,28 @@ export const selectHasPatientData = createSelector(
export const selectIsEmptyState = createSelector(
[selectPatients, selectPatientsLoading, selectFilteredPatients],
(patients, isLoading, filteredPatients) =>
!isLoading && patients.length > 0 && filteredPatients.length === 0
!isLoading && patients && Array.isArray(patients) && patients.length > 0 && filteredPatients.length === 0
);
/**
* Select Patient Counts for Filters
*
* Purpose: Get patient counts for each filter category
*/
export const selectPatientCounts = createSelector(
[selectPatients],
(patients) => {
if (!patients || !Array.isArray(patients)) {
return { all: 0, processed: 0, pending: 0, error: 0 };
}
return {
all: patients.length,
processed: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'processed').length,
pending: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'pending').length,
error: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'error').length,
};
}
);
/*

View File

@ -6,9 +6,77 @@
*/
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
import { MedicalCase, PatientCareState } from '../../../shared/types';
import { patientAPI } from '../services/patientAPI';
// ============================================================================
// TYPES
// ============================================================================
/**
* New API Response Types
*/
export interface SeriesSummary {
series_num: string;
series_description: string;
total_images: number;
png_preview: string;
modality: string;
}
export interface PatientInfo {
name: string;
age: string;
sex: string;
date: string;
institution: string;
modality: string;
status: string;
report_status: string;
file_name: string;
file_type: string;
frame_count: number;
}
export interface PatientData {
patid: string;
hospital_id: string;
first_processed_at: string;
last_processed_at: string;
total_files_processed: number;
patient_info: PatientInfo;
series_summary: SeriesSummary[];
processing_metadata: any;
}
export interface PatientCareState {
// Patients data
patients: PatientData[];
currentPatient: PatientData | null;
// Loading states
isLoading: boolean;
isRefreshing: boolean;
isLoadingPatientDetails: boolean;
// Error handling
error: string | null;
// Search and filtering
searchQuery: string;
selectedFilter: 'all' | 'processed' | 'pending' | 'error';
sortBy: 'date' | 'name' | 'processed';
sortOrder: 'asc' | 'desc';
// Pagination
currentPage: number;
itemsPerPage: number;
totalItems: number;
// Cache
lastUpdated: string | null;
cacheExpiry: string | null;
}
// ============================================================================
// ASYNC THUNKS
// ============================================================================
@ -24,84 +92,78 @@ export const fetchPatients = createAsyncThunk(
'patientCare/fetchPatients',
async (token: string, { rejectWithValue }) => {
try {
// Make actual API call to fetch medical cases
const response :any = await patientAPI.getPatients(token);
if (response.ok && response.data&&response.data.success) {
// Add random case types to each patient record
const caseTypes: Array<'Critical' | 'Emergency' | 'Routine'> = ['Critical', 'Emergency', 'Routine'];
const patientsWithTypes = response.data.data.map((patient: any) => ({
...patient,
type: caseTypes[Math.floor(Math.random() * caseTypes.length)]
}));
return patientsWithTypes as MedicalCase[];
const response: any = await patientAPI.getPatients(token);
console.log('response', response);
if (response.ok && response.data&& response.data.data) {
// Return the patients data directly from the new API structure
return response.data.data as PatientData[];
} else {
// Fallback to mock data for development
const mockPatients: MedicalCase[] = [
const mockPatients: PatientData[] = [
{
id: 1,
patientdetails: JSON.stringify({
patientdetails: {
Date: '2024-01-15',
Name: 'John Doe',
PatID: 'MRN001',
PatAge: '38',
PatSex: 'M',
Status: 'Active',
InstName: 'City General Hospital',
Modality: 'CT',
ReportStatus: 'Pending'
}
}),
series: JSON.stringify([
patid: "demo001",
hospital_id: "demo-hospital-001",
first_processed_at: "2025-01-15T10:30:00Z",
last_processed_at: "2025-01-15T11:45:00Z",
total_files_processed: 3,
patient_info: {
name: "John Doe",
age: "38",
sex: "M",
date: "2025-01-15",
institution: "City General Hospital",
modality: "CT",
status: "Processed",
report_status: "Available",
file_name: "chest_ct_001.dcm",
file_type: "dcm",
frame_count: 50
},
series_summary: [
{
Path: ['/dicom/series1'],
SerDes: 'Chest CT',
ViePos: 'Supine',
pngpath: '/images/ct_chest_1.png',
SeriesNum: '1',
ImgTotalinSeries: '50'
series_num: "1",
series_description: "Chest CT",
total_images: 50,
png_preview: "/images/ct_chest_1.png",
modality: "CT"
}
]),
created_at: '2024-01-15T10:30:00Z',
updated_at: '2024-01-15T11:45:00Z',
series_id: 'series_001',
type: 'Critical'
],
processing_metadata: {}
},
{
id: 2,
patientdetails: JSON.stringify({
patientdetails: {
Date: '2024-01-15',
Name: 'Jane Smith',
PatID: 'MRN002',
PatAge: '33',
PatSex: 'F',
Status: 'Active',
InstName: 'Memorial Medical Center',
Modality: 'MR',
ReportStatus: 'Completed'
}
}),
series: JSON.stringify([
patid: "demo002",
hospital_id: "demo-hospital-002",
first_processed_at: "2025-01-15T09:15:00Z",
last_processed_at: "2025-01-15T10:30:00Z",
total_files_processed: 2,
patient_info: {
name: "Jane Smith",
age: "33",
sex: "F",
date: "2025-01-15",
institution: "Memorial Medical Center",
modality: "MR",
status: "Processed",
report_status: "Available",
file_name: "brain_mri_001.dcm",
file_type: "dcm",
frame_count: 120
},
series_summary: [
{
Path: ['/dicom/series2'],
SerDes: 'Brain MRI',
ViePos: 'Supine',
pngpath: '/images/mri_brain_1.png',
SeriesNum: '2',
ImgTotalinSeries: '120'
series_num: "1",
series_description: "Brain MRI",
total_images: 120,
png_preview: "/images/mri_brain_1.png",
modality: "MR"
}
]),
created_at: '2024-01-15T09:15:00Z',
updated_at: '2024-01-15T10:30:00Z',
series_id: 'series_002',
type: 'Routine'
},
],
processing_metadata: {}
}
];
return mockPatients;
return [];
}
} catch (error: any) {
console.error('Fetch patients error:', error);
@ -126,35 +188,35 @@ export const fetchPatientDetails = createAsyncThunk(
await new Promise((resolve) => setTimeout(resolve as any, 1000));
// Mock patient details for specific patient
const mockPatient: MedicalCase = {
id: parseInt(patientId),
patientdetails: JSON.stringify({
patientdetails: {
Date: '2024-01-15',
Name: 'John Doe',
PatID: `MRN${patientId.padStart(3, '0')}`,
PatAge: '38',
PatSex: 'M',
Status: 'Active',
InstName: 'City General Hospital',
Modality: 'CT',
ReportStatus: 'Pending'
}
}),
series: JSON.stringify([
const mockPatient: PatientData = {
patid: patientId,
hospital_id: `demo-hospital-${patientId}`,
first_processed_at: "2025-01-15T10:30:00Z",
last_processed_at: "2025-01-15T11:45:00Z",
total_files_processed: 3,
patient_info: {
name: `Patient ${patientId}`,
age: "38",
sex: "M",
date: "2025-01-15",
institution: "City General Hospital",
modality: "CT",
status: "Processed",
report_status: "Available",
file_name: `patient_${patientId}.dcm`,
file_type: "dcm",
frame_count: 50
},
series_summary: [
{
Path: [`/dicom/series${patientId}`],
SerDes: 'Chest CT',
ViePos: 'Supine',
pngpath: `/images/ct_chest_${patientId}.png`,
SeriesNum: patientId,
ImgTotalinSeries: '50'
series_num: "1",
series_description: "Chest CT",
total_images: 50,
png_preview: `/images/ct_chest_${patientId}.png`,
modality: "CT"
}
]),
created_at: '2024-01-15T10:30:00Z',
updated_at: '2024-01-15T11:45:00Z',
series_id: `series_${patientId.padStart(3, '0')}`,
type: 'Critical'
],
processing_metadata: {}
};
return mockPatient;
@ -174,7 +236,7 @@ export const fetchPatientDetails = createAsyncThunk(
*/
export const updatePatient = createAsyncThunk(
'patientCare/updatePatient',
async (patientData: Partial<MedicalCase> & { id: number }, { rejectWithValue }) => {
async (patientData: Partial<PatientData> & { patid: string }, { rejectWithValue }) => {
try {
// TODO: Replace with actual API call
await new Promise((resolve) => setTimeout(resolve as any, 800));
@ -200,6 +262,7 @@ export const updatePatient = createAsyncThunk(
* - Loading states for async operations
* - Error handling and messages
* - Search and filtering
* - Pagination and caching
*/
const initialState: PatientCareState = {
// Patients data
@ -275,7 +338,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Set patient filter
*/
setFilter: (state, action: PayloadAction<'all' | 'Critical' | 'Routine' | 'Emergency'>) => {
setFilter: (state, action: PayloadAction<'all' | 'processed' | 'pending' | 'error'>) => {
state.selectedFilter = action.payload;
state.currentPage = 1; // Reset to first page when filtering
},
@ -285,7 +348,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Set patient sort options
*/
setSort: (state, action: PayloadAction<{ by: 'date' | 'name' | 'age'; order: 'asc' | 'desc' }>) => {
setSort: (state, action: PayloadAction<{ by: 'date' | 'name' | 'processed'; order: 'asc' | 'desc' }>) => {
state.sortBy = action.payload.by;
state.sortOrder = action.payload.order;
},
@ -314,7 +377,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Set the currently selected patient
*/
setCurrentPatient: (state, action: PayloadAction<MedicalCase | null>) => {
setCurrentPatient: (state, action: PayloadAction<PatientData | null>) => {
state.currentPatient = action.payload;
},
@ -323,14 +386,14 @@ const patientCareSlice = createSlice({
*
* Purpose: Update a patient in the patients list
*/
updatePatientInList: (state, action: PayloadAction<MedicalCase>) => {
const index = state.patients.findIndex(patient => patient.id === action.payload.id);
updatePatientInList: (state, action: PayloadAction<PatientData>) => {
const index = state.patients.findIndex(patient => patient.patid === action.payload.patid);
if (index !== -1) {
state.patients[index] = action.payload;
}
// Update current patient if it's the same patient
if (state.currentPatient && state.currentPatient.id === action.payload.id) {
if (state.currentPatient && state.currentPatient.patid === action.payload.patid) {
state.currentPatient = action.payload;
}
},
@ -340,7 +403,7 @@ const patientCareSlice = createSlice({
*
* Purpose: Add a new patient to the list
*/
addPatient: (state, action: PayloadAction<MedicalCase>) => {
addPatient: (state, action: PayloadAction<PatientData>) => {
state.patients.unshift(action.payload);
state.totalItems += 1;
},
@ -350,15 +413,15 @@ const patientCareSlice = createSlice({
*
* Purpose: Remove a patient from the list
*/
removePatient: (state, action: PayloadAction<number>) => {
const index = state.patients.findIndex(patient => patient.id === action.payload);
removePatient: (state, action: PayloadAction<string>) => {
const index = state.patients.findIndex(patient => patient.patid === action.payload);
if (index !== -1) {
state.patients.splice(index, 1);
state.totalItems -= 1;
}
// Clear current patient if it's the same patient
if (state.currentPatient && state.currentPatient.id === action.payload) {
if (state.currentPatient && state.currentPatient.patid === action.payload) {
state.currentPatient = null;
}
},
@ -415,13 +478,13 @@ const patientCareSlice = createSlice({
builder
.addCase(updatePatient.fulfilled, (state, action) => {
// Update patient in list
const index = state.patients.findIndex(patient => patient.id === action.payload.id);
const index = state.patients.findIndex(patient => patient.patid === action.payload.patid);
if (index !== -1) {
state.patients[index] = { ...state.patients[index], ...action.payload };
}
// Update current patient if it's the same patient
if (state.currentPatient && state.currentPatient.id === action.payload.id) {
if (state.currentPatient && state.currentPatient.patid === action.payload.patid) {
state.currentPatient = { ...state.currentPatient, ...action.payload };
}
})

File diff suppressed because it is too large Load Diff

View File

@ -5,61 +5,49 @@
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useEffect, useState, useCallback } from 'react';
import React, { useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
RefreshControl,
TouchableOpacity,
StatusBar,
Alert,
FlatList,
Dimensions,
RefreshControl,
SafeAreaView,
StatusBar,
StyleSheet,
Alert,
} from 'react-native';
import { theme } from '../../../theme/theme';
import { useNavigation } from '@react-navigation/native';
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import Icon from 'react-native-vector-icons/Feather';
import { SafeAreaView } from 'react-native-safe-area-context';
import { theme } from '../../../theme/theme';
// Import patient care functionality
import {
fetchPatients,
setSearchQuery,
setFilter,
setSort,
clearError
} from '../redux/patientCareSlice';
// Import patient care selectors
import {
selectPatients,
selectPatientsLoading,
selectPatientsError,
selectIsRefreshing,
selectSearchQuery,
selectSelectedFilter,
selectSortBy,
selectFilteredPatients,
} from '../redux/patientCareSelectors';
// Import auth selectors
import { selectUser } from '../../Auth/redux/authSelectors';
// Import components
// Components
import PatientCard from '../components/PatientCard';
import SearchBar from '../components/SearchBar';
import FilterTabs from '../components/FilterTabs';
import EmptyState from '../components/EmptyState';
import LoadingState from '../components/LoadingState';
import EmptyState from '../components/EmptyState';
// Import types
import { MedicalCase, PatientDetails, Series } from '../../../shared/types';
import { PatientsScreenProps } from '../navigation/navigationTypes';
// Redux
import {
fetchPatients,
setSearchQuery,
setFilter,
} from '../redux/patientCareSlice';
import {
selectPatients,
selectFilteredPatients,
selectPatientsLoading,
selectIsRefreshing,
selectPatientsError,
selectSearchQuery,
selectSelectedFilter,
selectPatientCounts,
} from '../redux/patientCareSelectors';
// Get screen dimensions
const { width: screenWidth } = Dimensions.get('window');
// Types
import { PatientData } from '../redux/patientCareSlice';
import { selectUser } from '../../Auth/redux/authSelectors';
// ============================================================================
// INTERFACES
@ -77,8 +65,8 @@ const { width: screenWidth } = Dimensions.get('window');
* Features:
* - Real-time patient data fetching
* - Search functionality with real-time filtering
* - Filter tabs (All, Active, Critical, Discharged)
* - Sort options (Priority, Name, Date)
* - Filter tabs (All, Processed, Pending, Error)
* - Sort options (Date, Name, Processed)
* - Pull-to-refresh functionality
* - Patient cards with vital information
* - Navigation to patient details
@ -86,12 +74,13 @@ const { width: screenWidth } = Dimensions.get('window');
* - Empty state handling
* - Modern ER-focused UI design
*/
const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
const PatientsScreen: React.FC = () => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const dispatch = useAppDispatch();
const navigation = useNavigation();
// Redux state
const patients = useAppSelector(selectPatients);
@ -101,85 +90,56 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
const error = useAppSelector(selectPatientsError);
const searchQuery = useAppSelector(selectSearchQuery);
const selectedFilter = useAppSelector(selectSelectedFilter);
const sortBy = useAppSelector(selectSortBy);
const patientCounts = useAppSelector(selectPatientCounts);
// Auth state
const user = useAppSelector(selectUser);
// Local state
const [showSortModal, setShowSortModal] = useState(false);
// ============================================================================
// LIFECYCLE METHODS
// EFFECTS
// ============================================================================
/**
* Component Mount Effect
* Fetch Patients on Mount
*
* Purpose: Initialize screen and fetch patient data
* Purpose: Load patients when component mounts
*/
useEffect(() => {
// Fetch patients on mount
handleFetchPatients();
// Set up navigation focus listener for real-time updates
const unsubscribe = navigation.addListener('focus', () => {
handleRefresh();
});
return unsubscribe;
}, [navigation]);
/**
* Error Handling Effect
*
* Purpose: Display error alerts and clear errors
*/
useEffect(() => {
if (error) {
Alert.alert(
'Error',
error,
[
{
text: 'Retry',
onPress: handleFetchPatients,
},
{
text: 'OK',
onPress: () => dispatch(clearError()),
},
]
);
}
}, [error]);
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Fetch Patients
*
* Purpose: Fetch patients from API
*/
const handleFetchPatients = useCallback(() => {
if (user?.access_token) {
dispatch(fetchPatients(user.access_token));
}
}, [dispatch, user?.access_token]);
/**
* Clear Error on Unmount
*
* Purpose: Clean up error state when component unmounts
*/
useEffect(() => {
return () => {
// No clearError action in this file, so this effect is removed.
};
}, [dispatch]);
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* Handle Refresh
*
* Purpose: Pull-to-refresh functionality
* Purpose: Handle pull-to-refresh functionality
*/
const handleRefresh = useCallback(() => {
handleFetchPatients();
}, [handleFetchPatients]);
if (user?.access_token) {
dispatch(fetchPatients(user.access_token));
}
}, [dispatch, user?.access_token]);
/**
* Handle Search
*
* Purpose: Handle search input changes
* Purpose: Handle search query changes
*
* @param query - Search query string
*/
@ -190,106 +150,36 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
/**
* Handle Filter Change
*
* Purpose: Handle filter tab selection
*
* @param filter - Selected filter option
* Purpose: Update the selected filter and refresh the list
*/
const handleFilterChange = useCallback((filter: 'all' | 'Critical' | 'Routine' | 'Emergency') => {
const handleFilterChange = useCallback((filter: 'all' | 'processed' | 'pending' | 'error') => {
dispatch(setFilter(filter));
}, [dispatch]);
/**
* Handle Sort Change
*
* Purpose: Handle sort option selection
*
* @param sortOption - Selected sort option
*/
const handleSortChange = useCallback((sortOption: 'date' | 'name' | 'age') => {
dispatch(setSort({ by: sortOption, order: 'desc' }));
setShowSortModal(false);
}, [dispatch]);
/**
* Handle Patient Press
*
* Purpose: Navigate to patient details screen
*
* @param patient - Selected patient
* Purpose: Navigate to patient details when a patient card is pressed
*/
const handlePatientPress = useCallback((patient: MedicalCase) => {
// Helper function to parse JSON strings safely
const parseJsonSafely = (jsonString: string | object) => {
if (typeof jsonString === 'object') {
return jsonString;
}
if (typeof jsonString === 'string') {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('Failed to parse JSON:', error);
return {};
}
}
return {};
};
const patientDetails = parseJsonSafely(patient.patientdetails);
const patientData = patientDetails.patientdetails || patientDetails;
navigation.navigate('PatientDetails', {
patientId:'1',
patientName: patientData.Name || 'Unknown Patient',
medicalCase: patient,
const handlePatientPress = useCallback((patient: PatientData) => {
(navigation as any).navigate('PatientDetails', {
patientId: patient.patid,
patientName: patient.patient_info.name,
});
}, [navigation]);
/**
* Handle Emergency Alert
*
* Purpose: Handle emergency alert for critical patients
*
* @param patient - Patient with emergency
* Purpose: Show emergency alert for critical patients
*/
const handleEmergencyAlert = useCallback((patient: MedicalCase) => {
// Helper function to parse JSON strings safely
const parseJsonSafely = (jsonString: string | object) => {
if (typeof jsonString === 'object') {
return jsonString;
}
if (typeof jsonString === 'string') {
try {
return JSON.parse(jsonString);
} catch (error) {
console.warn('Failed to parse JSON:', error);
return {};
}
}
return {};
};
const patientDetails = parseJsonSafely(patient.patientdetails);
const patientData = patientDetails.patientdetails || patientDetails;
const handleEmergencyAlert = useCallback((patient: PatientData) => {
Alert.alert(
'Emergency Alert',
`Critical status for ${patientData.Name || 'Unknown Patient'}\nID: ${patientData.PatID || 'N/A'}`,
`Patient ${patient.patient_info.name} (ID: ${patient.patid}) requires immediate attention!\n\nStatus: ${patient.patient_info.report_status}\nPriority: ${patient.patient_info.status}`,
[
{
text: 'View Details',
onPress: () => handlePatientPress(patient),
},
{
text: 'Call Physician',
onPress: () => {
// TODO: Implement physician calling functionality
Alert.alert('Calling', `Calling attending physician...`);
},
},
{
text: 'Cancel',
style: 'cancel',
},
{ text: 'Cancel', style: 'cancel' },
{ text: 'View Details', onPress: () => handlePatientPress(patient) },
]
);
}, [handlePatientPress]);
@ -299,18 +189,52 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
// ============================================================================
/**
* Render Patient Item
* Render Patient Card
*
* Purpose: Render individual patient card
*
* @param item - Patient data with render info
* Purpose: Render individual patient card component
*/
const renderPatientItem = ({ item }: { item: MedicalCase }) => (
const renderPatientCard = useCallback(({ item }: { item: PatientData }) => (
<PatientCard
patient={item}
onPress={() => handlePatientPress(item)}
onEmergencyPress={() => handleEmergencyAlert(item)}
/>
), [handlePatientPress, handleEmergencyAlert]);
/**
* Render Header
*
* Purpose: Render the screen header with title and action buttons
*/
const renderHeader = () => (
<View style={styles.header}>
<View style={styles.headerLeft}>
<Text style={styles.headerTitle}>Patients</Text>
<Text style={styles.headerSubtitle}>
{filteredPatients.length} of {patients?.length || 0} patients
</Text>
</View>
<View style={styles.headerRight}>
<TouchableOpacity
style={styles.actionButton}
onPress={() => {
// TODO: Implement sort modal
}}
>
<Text style={styles.actionButtonText}>Sort</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.actionButton}
onPress={() => {
// TODO: Implement filter modal
}}
>
<Text style={styles.actionButtonText}>Filter</Text>
</TouchableOpacity>
</View>
</View>
);
/**
@ -318,168 +242,143 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
*
* Purpose: Render empty state when no patients found
*/
const renderEmptyState = () => {
if (isLoading) return null;
return (
<EmptyState
title={searchQuery ? 'No patients found' : 'No patients available'}
subtitle={
searchQuery
? `No patients match "${searchQuery}"`
: 'Patients will appear here when available'
}
iconName="users"
onRetry={searchQuery ? undefined : handleFetchPatients}
/>
);
};
/**
* Render Loading State
*
* Purpose: Render loading state during initial fetch
*/
if (isLoading && patients.length === 0) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
<LoadingState
title="Loading Patients"
subtitle="Fetching patient data from server..."
/>
</SafeAreaView>
);
}
const renderEmptyState = () => (
<EmptyState
title="No Patients Found"
subtitle={searchQuery.trim() ?
`No patients match "${searchQuery}"` :
"No patients available at the moment"
}
iconName="users"
onRetry={handleRefresh}
retryText="Refresh"
/>
);
// ============================================================================
// MAIN RENDER
// ============================================================================
if (error && !isLoading) {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
<View style={styles.errorContainer}>
<Text style={styles.errorTitle}>Error Loading Patients</Text>
<Text style={styles.errorMessage}>{error}</Text>
<TouchableOpacity
style={styles.retryButton}
onPress={handleRefresh}
activeOpacity={0.7}
>
<Text style={styles.retryButtonText}>Retry</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
{/* Fixed Header */}
<View style={styles.header}>
<View style={styles.headerLeft}>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View>
<Text style={styles.headerTitle}>Patients</Text>
<Text style={styles.headerSubtitle}>Emergency Department</Text>
</View>
</View>
{/* Header */}
{renderHeader()}
{/* Search and Filters */}
<View style={styles.searchAndFilters}>
<SearchBar
value={searchQuery}
onChangeText={handleSearch}
placeholder="Search patients, ID, institution..."
/>
<View style={styles.headerRight}>
<TouchableOpacity
style={styles.headerButton}
onPress={handleRefresh}
disabled={isRefreshing}
>
<Icon
name="refresh-cw"
size={20}
color={isRefreshing ? theme.colors.textMuted : theme.colors.primary}
<FilterTabs
selectedFilter={selectedFilter}
onFilterChange={handleFilterChange}
patientCounts={patientCounts}
/>
</View>
{/* Loading State */}
{isLoading && patients.length === 0 && (
<View style={styles.centerContainer}>
<LoadingState />
</View>
)}
{/* Error State */}
{error && patients.length === 0 && (
<View style={styles.centerContainer}>
<EmptyState
iconName="alert-circle"
title="Error Loading Patients"
subtitle={error}
retryText="Retry"
onRetry={handleRefresh}
/>
</View>
)}
{/* Empty State */}
{!isLoading && !error && patients.length === 0 && (
<View style={styles.centerContainer}>
<EmptyState
iconName="users"
title="No Patients Found"
subtitle="There are no patients in the system yet."
retryText="Refresh"
onRetry={handleRefresh}
/>
</View>
)}
{/* No Results State */}
{!isLoading && !error && patients.length > 0 && filteredPatients.length === 0 && (
<View style={styles.centerContainer}>
<EmptyState
iconName="search"
title="No Results Found"
subtitle={`No patients match your search "${searchQuery}" and filter "${selectedFilter}"`}
retryText="Clear Search"
onRetry={() => {
handleSearch('');
handleFilterChange('all');
}}
/>
</View>
)}
{/* Patient List */}
{!isLoading && !error && filteredPatients.length > 0 && (
<FlatList
data={filteredPatients}
renderItem={renderPatientCard}
keyExtractor={(item) => item.patid}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
colors={[theme.colors.primary]}
tintColor={theme.colors.primary}
/>
</TouchableOpacity>
<TouchableOpacity
style={styles.headerButton}
onPress={() => {
// TODO: Implement notifications screen
Alert.alert('Notifications', 'Notifications feature coming soon');
}}
>
<Icon name="bell" size={20} color={theme.colors.textSecondary} />
{/* Notification badge */}
<View style={styles.notificationBadge}>
<Text style={styles.badgeText}>3</Text>
}
ListFooterComponent={
<View style={styles.listFooter}>
<Text style={styles.footerText}>
Showing {filteredPatients.length} of {patients.length} patients
</Text>
</View>
</TouchableOpacity>
</View>
</View>
{/* Fixed Search and Filter Section */}
<View style={styles.fixedSection}>
{/* Search Bar */}
<View style={styles.searchContainer}>
<SearchBar
value={searchQuery}
onChangeText={handleSearch}
placeholder="Search patients by name, MRN, or room..."
showFilter
onFilterPress={() => setShowSortModal(true)}
/>
</View>
{/* Filter Tabs */}
<View style={styles.filterContainer}>
<FilterTabs
selectedFilter={selectedFilter}
onFilterChange={handleFilterChange}
patientCounts={{
all: patients.length,
Critical: patients.filter((p: MedicalCase) => p.type === 'Critical').length,
Routine: patients.filter((p: MedicalCase) => p.type === 'Routine').length,
Emergency: patients.filter((p: MedicalCase) => p.type === 'Emergency').length,
}}
/>
</View>
{/* Results Summary */}
<View style={styles.resultsSummary}>
<View style={styles.resultsLeft}>
<Icon name="users" size={16} color={theme.colors.textSecondary} />
<Text style={styles.resultsText}>
{filteredPatients.length} patient{filteredPatients.length !== 1 ? 's' : ''} found
</Text>
</View>
<View style={styles.sortInfo}>
<Icon name="filter" size={14} color={theme.colors.textMuted} />
<Text style={styles.sortText}>
Sorted by {sortBy}
</Text>
</View>
</View>
</View>
{/* Scrollable Patient List Only */}
<FlatList
data={filteredPatients}
renderItem={renderPatientItem}
keyExtractor={(item,index) => index.toString()}
ListEmptyComponent={renderEmptyState}
contentContainerStyle={[
styles.listContent,
filteredPatients.length === 0 && styles.emptyListContent
]}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
colors={[theme.colors.primary]}
tintColor={theme.colors.primary}
/>
}
// Performance optimizations
// removeClippedSubviews={true}
// maxToRenderPerBatch={10}
// windowSize={10}
// initialNumToRender={8}
// getItemLayout={(data, index) => ({
// length: 120, // Approximate height of PatientCard
// offset: 120 * index,
// index,
// })}
/>
}
/>
)}
{/* TODO: Implement sort and filter modals for enhanced functionality */}
{/* Note: Patient data will be loaded from API when fetchPatients is called */}
{/* Currently using mock data from Redux slice for development */}
</SafeAreaView>
);
};
@ -489,6 +388,7 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
// ============================================================================
const styles = StyleSheet.create({
// Container Styles
container: {
flex: 1,
backgroundColor: theme.colors.background,
@ -500,19 +400,17 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
paddingVertical: theme.spacing.md,
backgroundColor: theme.colors.background,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
headerLeft: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
backButton: {
marginRight: theme.spacing.sm,
padding: theme.spacing.xs,
headerRight: {
flexDirection: 'row',
gap: theme.spacing.sm,
},
headerTitle: {
fontSize: 24,
@ -523,89 +421,89 @@ const styles = StyleSheet.create({
headerSubtitle: {
fontSize: 14,
color: theme.colors.textSecondary,
fontFamily: theme.typography.fontFamily.bold,
fontFamily: theme.typography.fontFamily.regular,
},
headerRight: {
flexDirection: 'row',
alignItems: 'center',
},
headerButton: {
padding: theme.spacing.sm,
marginLeft: theme.spacing.xs,
position: 'relative',
},
notificationBadge: {
position: 'absolute',
top: 6,
right: 6,
backgroundColor: theme.colors.error,
actionButton: {
backgroundColor: theme.colors.backgroundAlt,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.sm,
borderRadius: 8,
width: 16,
height: 16,
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: theme.colors.border,
},
badgeText: {
color: theme.colors.background,
fontSize: 10,
fontWeight: 'bold',
actionButtonText: {
color: theme.colors.textSecondary,
fontSize: 14,
fontWeight: '600',
fontFamily: theme.typography.fontFamily.medium,
},
// Fixed Section Styles
fixedSection: {
// Search and Filters
searchAndFilters: {
paddingHorizontal: theme.spacing.md,
paddingTop: theme.spacing.sm,
paddingBottom: theme.spacing.md,
paddingBottom: theme.spacing.sm,
backgroundColor: theme.colors.background,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
searchContainer: {
marginBottom: theme.spacing.md,
},
filterContainer: {
marginBottom: theme.spacing.sm,
// Center Container for States
centerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: theme.spacing.md,
},
// List Styles
listContent: {
paddingTop: theme.spacing.sm,
paddingBottom: theme.spacing.xl,
listContainer: {
paddingBottom: theme.spacing.lg,
},
emptyListContent: {
flexGrow: 1,
listFooter: {
paddingVertical: theme.spacing.md,
alignItems: 'center',
},
footerText: {
fontSize: 14,
color: theme.colors.textMuted,
fontFamily: theme.typography.fontFamily.regular,
},
// Results Summary
resultsSummary: {
flexDirection: 'row',
justifyContent: 'space-between',
// Error State Styles
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: theme.spacing.sm,
paddingHorizontal: theme.spacing.sm,
backgroundColor: theme.colors.backgroundAlt,
borderRadius: 8,
marginTop: theme.spacing.xs,
padding: theme.spacing.xl,
},
resultsLeft: {
flexDirection: 'row',
alignItems: 'center',
errorTitle: {
fontSize: 20,
fontWeight: 'bold',
color: theme.colors.error,
marginBottom: theme.spacing.sm,
fontFamily: theme.typography.fontFamily.bold,
textAlign: 'center',
},
resultsText: {
fontSize: 14,
color: theme.colors.textPrimary,
fontWeight: '500',
marginLeft: theme.spacing.xs,
},
sortInfo: {
flexDirection: 'row',
alignItems: 'center',
},
sortText: {
fontSize: 12,
errorMessage: {
fontSize: 16,
color: theme.colors.textSecondary,
textTransform: 'capitalize',
marginLeft: theme.spacing.xs,
marginBottom: theme.spacing.lg,
fontFamily: theme.typography.fontFamily.regular,
textAlign: 'center',
},
retryButton: {
backgroundColor: theme.colors.primary,
paddingHorizontal: theme.spacing.lg,
paddingVertical: theme.spacing.md,
borderRadius: 8,
minWidth: 120,
alignItems: 'center',
},
retryButtonText: {
color: theme.colors.background,
fontSize: 16,
fontWeight: '600',
fontFamily: theme.typography.fontFamily.medium,
},
});

View File

@ -34,11 +34,24 @@ export const patientAPI = {
* @returns Promise with medical cases data
*/
getPatients: (token: string) => {
return api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data', {}, buildHeaders({ token }));
return api.get('/api/ai-cases/all-patients', {}, buildHeaders({ token }));
},
/**
* Get Patient Details
* Get Patient Details by ID
*
* Purpose: Fetch detailed information for a specific patient by ID
*
* @param patientId - Patient ID
* @param token - Authentication token
* @returns Promise with patient details including predictions and series
*/
getPatientDetailsById: (patientId: string, token: string) => {
return api.get(`/api/ai-cases/patient/${patientId}/predictions`, {}, buildHeaders({ token }));
},
/**
* Get Patient Details (Legacy - kept for backward compatibility)
*
* Purpose: Fetch detailed information for a specific patient
*
@ -47,7 +60,7 @@ export const patientAPI = {
* @returns Promise with patient details
*/
getPatientDetails: (patientId: string, token: string) => {
return api.get(`/api/patients/${patientId}`, {}, buildHeaders({ token }));
return api.get(`/api/ai-cases/patient/${patientId}/predictions`, {}, buildHeaders({ token }));
},
/**
@ -88,9 +101,24 @@ export const patientAPI = {
* @returns Promise with updated vital signs
*/
updatePatientVitals: (patientId: string, vitalSigns: any, token: string) => {
return api.post(`/api/patients/${patientId}/vitals`, vitalSigns, buildHeaders({ token }));
return api.put(`/api/patients/${patientId}/vitals`, vitalSigns, buildHeaders({ token }));
},
/**
* Submit Feedback for AI Prediction
*
* Purpose: Submit physician feedback for AI predictions
*
* @param feedbackData - Feedback payload
* @returns API response
*/
submitFeedback: (feedbackData: {
patid: string;
prediction_id: number;
feedback_text: string;
is_positive: boolean;
},token) => api.post('/api/ai-cases/feedbacks', feedbackData, buildHeaders({ token })),
/**
* Get Patient Medical History
*

View File

@ -6,7 +6,8 @@
*/
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme/theme';
/**
@ -16,9 +17,13 @@ import { theme } from '../../../theme/theme';
*
* Props:
* - title: Title text to display in the header
* - showBackButton: Whether to show the back button (optional)
* - onBackPress: Function to call when back button is pressed (optional)
*/
interface SettingsHeaderProps {
title: string;
showBackButton?: boolean;
onBackPress?: () => void;
}
/**
@ -31,9 +36,22 @@ interface SettingsHeaderProps {
* - Consistent with app theme
* - Proper spacing and typography
*/
export const SettingsHeader: React.FC<SettingsHeaderProps> = ({ title }) => {
export const SettingsHeader: React.FC<SettingsHeaderProps> = ({
title,
showBackButton = false,
onBackPress
}) => {
return (
<View style={styles.container}>
{showBackButton && onBackPress && (
<TouchableOpacity
style={styles.backButton}
onPress={onBackPress}
activeOpacity={0.7}
>
<Icon name="chevron-left" size={24} color={theme.colors.primary} />
</TouchableOpacity>
)}
<Text style={styles.title}>{title}</Text>
</View>
);
@ -51,6 +69,14 @@ const styles = StyleSheet.create({
paddingVertical: theme.spacing.lg,
borderBottomColor: theme.colors.border,
borderBottomWidth: 1,
flexDirection: 'row',
alignItems: 'center',
},
// Back button styling
backButton: {
marginRight: theme.spacing.md,
padding: theme.spacing.xs,
},
// Title text styling
@ -58,6 +84,7 @@ const styles = StyleSheet.create({
fontSize: theme.typography.fontSize.displayMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
flex: 1,
},
});

View File

@ -14,6 +14,7 @@ import { SettingsScreen } from '../screens/SettingsScreen';
// Import navigation types
import { SettingsStackParamList } from './navigationTypes';
import { theme } from '../../../theme';
import { AppInfoScreen, ChangePasswordScreen, EditProfileScreen } from '../screens';
// Create stack navigator for Settings module
const Stack = createStackNavigator<SettingsStackParamList>();
@ -78,6 +79,38 @@ const SettingsStackNavigator: React.FC = () => {
headerShown: false, // Hide header for main settings screen
}}
/>
<Stack.Screen
name="AppInfoScreen"
component={AppInfoScreen}
options={{
title: 'App Info',
headerShown: false,
}}
/>
<Stack.Screen
name="EditProfileScreen"
component={EditProfileScreen}
options={{
title: 'Edit Profile',
headerShown: false,
}}
/>
<Stack.Screen
name="ChangePasswordScreen"
component={ChangePasswordScreen}
options={{
title: 'Change Password',
headerShown: false,
}}
/>
{/* <Stack.Screen
name="HelpSupportScreen"
component={HelpSupportScreen}
options={{
title: 'Help & Support',
headerShown: false,
}}
/> */}
</Stack.Navigator>
);
};

View File

@ -19,28 +19,21 @@ export type SettingsStackParamList = {
SettingScreen: SettingsScreenParams;
// Profile Edit screen - Edit user profile information
ProfileEdit: ProfileEditScreenParams;
// ProfileEdit: ProfileEditScreenParams;
// Security Settings screen - Security and privacy settings
SecuritySettings: SecuritySettingsScreenParams;
// Notification Settings screen - Notification preferences
NotificationSettings: NotificationSettingsScreenParams;
// Clinical Preferences screen - Clinical workflow preferences
ClinicalPreferences: ClinicalPreferencesScreenParams;
// Privacy Settings screen - Privacy and data settings
PrivacySettings: PrivacySettingsScreenParams;
// Accessibility Settings screen - Accessibility preferences
AccessibilitySettings: AccessibilitySettingsScreenParams;
// About screen - App information and version
About: AboutScreenParams;
AppInfoScreen: AppInfoScreenParams;
// Change Password screen - Change user password
ChangePasswordScreen: ChangePasswordScreenParams;
// Edit Profile screen - Edit user profile information
EditProfileScreen: EditProfileScreenParams;
// Help & Support screen - Help documentation and support
HelpSupport: HelpSupportScreenParams;
// HelpSupport: HelpSupportScreenParams;
};
/**
@ -238,7 +231,21 @@ export type AccessibilitySettingsScreenProps = SettingsScreenProps<'Accessibilit
/**
* AboutScreenProps - Props for AboutScreen component
*/
export type AboutScreenProps = SettingsScreenProps<'About'>;
export type AppInfoScreenParams = SettingsScreenProps<'AppInfoScreen'>
/**
* ChangePasswordScreenProps - Props for ChangePasswordScreen component
*/
export type ChangePasswordScreenProps = SettingsScreenProps<'ChangePasswordScreen'>;
/**
* EditProfileScreenProps - Props for EditProfileScreen component
*/
export type EditProfileScreenProps = SettingsScreenProps<'EditProfileScreen'>;
/**
* HelpSupportScreenProps - Props for HelpSupportScreen component

View File

@ -0,0 +1,486 @@
/*
* File: AppInfoScreen.tsx
* Description: App information screen displaying version, build, and app details
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
Image,
Linking,
TouchableOpacity,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme/theme';
import { SettingsHeader } from '../components/SettingsHeader';
import { CustomModal } from '../../../shared/components';
/**
* AppInfoScreenProps Interface
*
* Purpose: Defines the props required by the AppInfoScreen component
*
* Props:
* - navigation: React Navigation object for screen navigation
*/
interface AppInfoScreenProps {
navigation: any;
}
/**
* AppInfoScreen Component
*
* Purpose: Display comprehensive app information including version, build details,
* and links to legal documents and support resources
*
* Features:
* 1. App logo and basic information
* 2. Version and build details
* 3. Legal and privacy information
* 4. Support and contact links
* 5. App description and features
*/
export const AppInfoScreen: React.FC<AppInfoScreenProps> = ({
navigation,
}) => {
// ============================================================================
// APP INFORMATION DATA
// ============================================================================
// App version and build information
const appInfo = {
name: 'NeoScan Radiologist',
version: '1.0.0',
buildNumber: '2025.08.001',
releaseDate: 'August 2025',
developer: 'Tech4Biz Solutions',
copyright: '© 2024 Spurrin Innovations. All rights reserved.',
};
// App features and description
const appDescription = {
title: 'Emergency Radiology Physician App',
description: 'Advanced medical imaging and patient management platform designed specifically for emergency room physicians. Provides real-time access to critical patient scans, AI-powered diagnostic assistance, and streamlined clinical workflows.',
features: [
'Real-time patient monitoring',
'AI-powered diagnostic assistance',
'Emergency alert system',
'Secure patient data management',
'Mobile-optimized interface',
'Hospital system integration',
],
};
// Legal and support information
const legalInfo = {
privacyPolicy: 'https://neoscan.com/privacy',
termsOfService: 'https://neoscan.com/terms',
supportEmail: 'support@neoscan.com',
supportPhone: '+1-800-NEOSCAN',
website: 'https://neoscan.com',
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* handleExternalLink Function
*
* Purpose: Handle opening external links in browser
*
* @param url - URL to open
*/
const handleExternalLink = async (url: string) => {
try {
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
console.log("Can't open URL:", url);
}
} catch (error) {
console.error('Error opening URL:', error);
}
};
/**
* handleEmailSupport Function
*
* Purpose: Handle opening email client for support
*/
const handleEmailSupport = () => {
const emailUrl = `mailto:${legalInfo.supportEmail}?subject=NeoScan Physician App Support`;
handleExternalLink(emailUrl);
};
/**
* handlePhoneSupport Function
*
* Purpose: Handle opening phone dialer for support
*/
const handlePhoneSupport = () => {
const phoneUrl = `tel:${legalInfo.supportPhone}`;
handleExternalLink(phoneUrl);
};
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<View style={styles.container}>
{/* App info header with back button */}
<SettingsHeader
title="App Information"
showBackButton={true}
onBackPress={() => navigation.goBack()}
/>
{/* Scrollable app information content */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* App logo and basic info section */}
<View style={styles.appHeaderSection}>
<View style={styles.appLogoContainer}>
<View style={styles.appLogo}>
<Text style={styles.appLogoText}>NS</Text>
</View>
</View>
<Text style={styles.appName}>{appInfo.name}</Text>
<Text style={styles.appVersion}>Version {appInfo.version}</Text>
<Text style={styles.appBuild}>Build {appInfo.buildNumber}</Text>
<Text style={styles.appReleaseDate}>{appInfo.releaseDate}</Text>
</View>
{/* App description section */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>{appDescription.title}</Text>
<Text style={styles.appDescription}>{appDescription.description}</Text>
<Text style={styles.featuresTitle}>Key Features:</Text>
{appDescription.features.map((feature, index) => (
<View key={index} style={styles.featureItem}>
<View style={styles.featureBullet} />
<Text style={styles.featureText}>{feature}</Text>
</View>
))}
</View>
{/* Version and build details section */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Technical Information</Text>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>App Version:</Text>
<Text style={styles.infoValue}>{appInfo.version}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Build Number:</Text>
<Text style={styles.infoValue}>{appInfo.buildNumber}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Release Date:</Text>
<Text style={styles.infoValue}>{appInfo.releaseDate}</Text>
</View>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>Developer:</Text>
<Text style={styles.infoValue}>{appInfo.developer}</Text>
</View>
</View>
{/* Support and contact section */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Support & Contact</Text>
<TouchableOpacity
style={styles.contactItem}
onPress={handleEmailSupport}
activeOpacity={0.7}
>
<Text style={styles.contactLabel}>Email Support:</Text>
<Text style={styles.contactValue}>{legalInfo.supportEmail}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.contactItem}
onPress={handlePhoneSupport}
activeOpacity={0.7}
>
<Text style={styles.contactLabel}>Phone Support:</Text>
<Text style={styles.contactValue}>{legalInfo.supportPhone}</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.contactItem}
onPress={() => handleExternalLink(legalInfo.website)}
activeOpacity={0.7}
>
<Text style={styles.contactLabel}>Website:</Text>
<Text style={styles.contactValue}>{legalInfo.website}</Text>
</TouchableOpacity>
</View>
{/* Legal information section */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Legal Information</Text>
<TouchableOpacity
style={styles.legalItem}
onPress={() => handleExternalLink(legalInfo.privacyPolicy)}
activeOpacity={0.7}
>
<Text style={styles.legalText}>Privacy Policy</Text>
<Icon name="chevron-right" size={20} color={theme.colors.primary} />
</TouchableOpacity>
<TouchableOpacity
style={styles.legalItem}
onPress={() => handleExternalLink(legalInfo.termsOfService)}
activeOpacity={0.7}
>
<Text style={styles.legalText}>Terms of Service</Text>
<Icon name="chevron-right" size={20} color={theme.colors.primary} />
</TouchableOpacity>
</View>
{/* Copyright section */}
<View style={styles.copyrightSection}>
<Text style={styles.copyrightText}>{appInfo.copyright}</Text>
</View>
{/* Bottom spacing for tab bar */}
<View style={styles.bottomSpacing} />
</ScrollView>
</View>
);
};
// ============================================================================
// STYLES SECTION
// ============================================================================
const styles = StyleSheet.create({
// Main container for the app info screen
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view styling
scrollView: {
flex: 1,
},
// Scroll content styling
scrollContent: {
paddingHorizontal: theme.spacing.md,
},
// Bottom spacing for tab bar
bottomSpacing: {
height: theme.spacing.xl,
},
// App header section with logo and basic info
appHeaderSection: {
alignItems: 'center',
paddingVertical: theme.spacing.xl,
marginBottom: theme.spacing.md,
},
appLogoContainer: {
marginBottom: theme.spacing.md,
},
appLogo: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: theme.colors.primary,
justifyContent: 'center',
alignItems: 'center',
...theme.shadows.medium,
},
appLogoText: {
color: theme.colors.background,
fontSize: 32,
fontFamily: theme.typography.fontFamily.bold,
},
appName: {
fontSize: theme.typography.fontSize.displayMedium,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
textAlign: 'center',
},
appVersion: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.primary,
marginBottom: theme.spacing.xs,
},
appBuild: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
appReleaseDate: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Information sections
infoSection: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.md,
},
appDescription: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
lineHeight: 22,
marginBottom: theme.spacing.md,
},
featuresTitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
featureItem: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: theme.spacing.xs,
},
featureBullet: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: theme.colors.primary,
marginRight: theme.spacing.sm,
},
featureText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
flex: 1,
},
// Information rows
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: theme.spacing.xs,
borderBottomColor: theme.colors.border,
borderBottomWidth: 1,
},
infoLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
},
infoValue: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Contact items
contactItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: theme.spacing.sm,
borderBottomColor: theme.colors.border,
borderBottomWidth: 1,
},
contactLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
},
contactValue: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.primary,
},
// Legal items
legalItem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: theme.spacing.sm,
borderBottomColor: theme.colors.border,
borderBottomWidth: 1,
},
legalText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.primary,
},
// Copyright section
copyrightSection: {
alignItems: 'center',
paddingVertical: theme.spacing.lg,
marginBottom: theme.spacing.md,
},
copyrightText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
textAlign: 'center',
lineHeight: 18,
},
});
/*
* End of File: AppInfoScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,826 @@
/*
* File: ChangePasswordScreen.tsx
* Description: Change password screen with comprehensive password validation
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TextInput,
TouchableOpacity,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme/theme';
import { SettingsHeader } from '../components/SettingsHeader';
import { useAppDispatch } from '../../../store/hooks';
import { changePasswordAsync } from '../../Auth/redux/authActions';
/**
* ChangePasswordScreenProps Interface
*
* Purpose: Defines the props required by the ChangePasswordScreen component
*
* Props:
* - navigation: React Navigation object for screen navigation
*/
interface ChangePasswordScreenProps {
navigation: any;
}
/**
* FormData Interface
*
* Purpose: Defines the structure of the password change form data
*/
interface FormData {
currentPassword: string;
newPassword: string;
confirmPassword: string;
}
/**
* FormErrors Interface
*
* Purpose: Defines the structure of form validation errors
*/
interface FormErrors {
currentPassword?: string;
newPassword?: string;
confirmPassword?: string;
}
/**
* PasswordStrength Interface
*
* Purpose: Defines the structure of password strength information
*/
interface PasswordStrength {
score: number;
label: string;
color: string;
requirements: string[];
}
/**
* ChangePasswordScreen Component
*
* Purpose: Allows users to change their password with comprehensive validation
* including current password verification, new password strength requirements,
* and password confirmation
*
* Features:
* 1. Current password verification
* 2. New password strength validation
* 3. Password confirmation matching
* 4. Real-time password strength indicator
* 5. Comprehensive error handling
*/
export const ChangePasswordScreen: React.FC<ChangePasswordScreenProps> = ({
navigation,
}) => {
// ============================================================================
// REDUX STATE
// ============================================================================
const dispatch = useAppDispatch();
// ============================================================================
// LOCAL STATE
// ============================================================================
const [formData, setFormData] = useState<FormData>({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [passwordStrength, setPasswordStrength] = useState<PasswordStrength>({
score: 0,
label: 'Very Weak',
color: theme.colors.error,
requirements: [],
});
// ============================================================================
// PASSWORD STRENGTH VALIDATION
// ============================================================================
/**
* checkPasswordStrength Function
*
* Purpose: Analyze password strength and return strength information
*
* @param password - Password to analyze
* @returns PasswordStrength object with score, label, color, and requirements
*/
const checkPasswordStrength = (password: string): PasswordStrength => {
const requirements: string[] = [];
let score = 0;
// Length requirement
if (password.length >= 8) {
score += 1;
requirements.push('✓ At least 8 characters');
} else {
requirements.push('✗ At least 8 characters');
}
// Uppercase requirement
if (/[A-Z]/.test(password)) {
score += 1;
requirements.push('✓ Contains uppercase letter');
} else {
requirements.push('✗ Contains uppercase letter');
}
// Lowercase requirement
if (/[a-z]/.test(password)) {
score += 1;
requirements.push('✓ Contains lowercase letter');
} else {
requirements.push('✗ Contains lowercase letter');
}
// Number requirement
if (/\d/.test(password)) {
score += 1;
requirements.push('✓ Contains number');
} else {
requirements.push('✗ Contains number');
}
// Special character requirement
if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
score += 1;
requirements.push('✓ Contains special character');
} else {
requirements.push('✗ Contains special character');
}
// Determine strength label and color
let label: string;
let color: string;
if (score <= 1) {
label = 'Very Weak';
color = theme.colors.error;
} else if (score <= 2) {
label = 'Weak';
color = theme.colors.warning;
} else if (score <= 3) {
label = 'Fair';
color = theme.colors.warning;
} else if (score <= 4) {
label = 'Good';
color = theme.colors.info;
} else {
label = 'Strong';
color = theme.colors.success;
}
return {
score,
label,
color,
requirements,
};
};
// ============================================================================
// VALIDATION FUNCTIONS
// ============================================================================
/**
* validateField Function
*
* Purpose: Validate individual form fields
*
* @param field - Field name to validate
* @param value - Field value to validate
* @returns Validation error message or undefined
*/
const validateField = (field: keyof FormData, value: string): string | undefined => {
switch (field) {
case 'currentPassword':
if (!value.trim()) {
return 'Current password is required';
}
if (value.trim().length < 6) {
return 'Current password must be at least 6 characters';
}
break;
case 'newPassword':
if (!value.trim()) {
return 'New password is required';
}
if (value.trim().length < 8) {
return 'New password must be at least 8 characters';
}
if (value === formData.currentPassword) {
return 'New password must be different from current password';
}
break;
case 'confirmPassword':
if (!value.trim()) {
return 'Please confirm your new password';
}
if (value !== formData.newPassword) {
return 'Passwords do not match';
}
break;
}
return undefined;
};
/**
* validateForm Function
*
* Purpose: Validate entire form and return validation errors
*
* @returns Object containing validation errors
*/
const validateForm = (): FormErrors => {
const newErrors: FormErrors = {};
Object.keys(formData).forEach((field) => {
const key = field as keyof FormData;
const error = validateField(key, formData[key]);
if (error) {
newErrors[key] = error;
}
});
return newErrors;
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* handleInputChange Function
*
* Purpose: Handle input field changes and update password strength
*
* @param field - Field name that changed
* @param value - New field value
*/
const handleInputChange = (field: keyof FormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear field-specific error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
// Update password strength for new password field
if (field === 'newPassword') {
const strength = checkPasswordStrength(value);
setPasswordStrength(strength);
}
};
/**
* handleInputBlur Function
*
* Purpose: Validate field when user leaves the input
*
* @param field - Field name to validate
*/
const handleInputBlur = (field: keyof FormData) => {
const error = validateField(field, formData[field]);
setErrors(prev => ({ ...prev, [field]: error }));
};
/**
* handleSubmit Function
*
* Purpose: Handle form submission with validation and API call
*/
const handleSubmit = async () => {
// Validate form
const validationErrors = validateForm();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
// Check password strength
if (passwordStrength.score < 3) {
Alert.alert(
'Weak Password',
'Please choose a stronger password that meets the requirements.',
[{ text: 'OK' }]
);
return;
}
setIsSubmitting(true);
try {
// Dispatch password change action
await dispatch(changePasswordAsync({
currentPassword: formData.currentPassword,
newPassword: formData.newPassword,
})).unwrap();
// Navigate back after successful password change
navigation.goBack();
} catch (error: any) {
// Handle error - toast notification is already shown in the thunk
console.error('Password change error:', error);
} finally {
setIsSubmitting(false);
}
};
/**
* handleCancel Function
*
* Purpose: Handle cancel action
*/
const handleCancel = () => {
navigation.goBack();
};
/**
* togglePasswordVisibility Function
*
* Purpose: Toggle password visibility for specified field
*
* @param field - Field to toggle visibility for
*/
const togglePasswordVisibility = (field: 'current' | 'new' | 'confirm') => {
switch (field) {
case 'current':
setShowCurrentPassword(!showCurrentPassword);
break;
case 'new':
setShowNewPassword(!showNewPassword);
break;
case 'confirm':
setShowConfirmPassword(!showConfirmPassword);
break;
}
};
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
{/* Header with back button */}
<SettingsHeader
title="Change Password"
showBackButton={true}
onBackPress={handleCancel}
/>
{/* Scrollable form content */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* Password requirements info */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Password Requirements</Text>
<Text style={styles.infoText}>
Your new password must meet the following requirements to ensure security:
</Text>
<View style={styles.requirementsList}>
<Text style={styles.requirementItem}> At least 8 characters long</Text>
<Text style={styles.requirementItem}> Contains uppercase and lowercase letters</Text>
<Text style={styles.requirementItem}> Contains at least one number</Text>
<Text style={styles.requirementItem}> Contains at least one special character</Text>
</View>
</View>
{/* Password change form */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Change Password</Text>
{/* Current Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Current Password *</Text>
<View style={styles.passwordInputContainer}>
<TextInput
style={[
styles.textInput,
styles.passwordInput,
errors.currentPassword ? styles.inputError : null,
]}
value={formData.currentPassword}
onChangeText={(value) => handleInputChange('currentPassword', value)}
onBlur={() => handleInputBlur('currentPassword')}
placeholder="Enter your current password"
placeholderTextColor={theme.colors.textMuted}
secureTextEntry={!showCurrentPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.eyeIcon}
onPress={() => togglePasswordVisibility('current')}
activeOpacity={0.7}
>
<Icon
name={showCurrentPassword ? 'eye-off' : 'eye'}
size={20}
color={theme.colors.textMuted}
/>
</TouchableOpacity>
</View>
{errors.currentPassword && (
<Text style={styles.errorText}>{errors.currentPassword}</Text>
)}
</View>
{/* New Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>New Password *</Text>
<View style={styles.passwordInputContainer}>
<TextInput
style={[
styles.textInput,
styles.passwordInput,
errors.newPassword ? styles.inputError : null,
]}
value={formData.newPassword}
onChangeText={(value) => handleInputChange('newPassword', value)}
onBlur={() => handleInputBlur('newPassword')}
placeholder="Enter your new password"
placeholderTextColor={theme.colors.textMuted}
secureTextEntry={!showNewPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.eyeIcon}
onPress={() => togglePasswordVisibility('new')}
activeOpacity={0.7}
>
<Icon
name={showNewPassword ? 'eye-off' : 'eye'}
size={20}
color={theme.colors.textMuted}
/>
</TouchableOpacity>
</View>
{errors.newPassword && (
<Text style={styles.errorText}>{errors.newPassword}</Text>
)}
</View>
{/* Password Strength Indicator */}
{formData.newPassword.length > 0 && (
<View style={styles.strengthContainer}>
<Text style={styles.strengthLabel}>Password Strength:</Text>
<View style={styles.strengthBar}>
<View
style={[
styles.strengthProgress,
{
width: `${(passwordStrength.score / 5) * 100}%`,
backgroundColor: passwordStrength.color,
}
]}
/>
</View>
<Text style={[styles.strengthText, { color: passwordStrength.color }]}>
{passwordStrength.label}
</Text>
</View>
)}
{/* Password Requirements Check */}
{formData.newPassword.length > 0 && (
<View style={styles.requirementsCheck}>
{passwordStrength.requirements.map((requirement, index) => (
<Text key={index} style={styles.requirementCheckItem}>
{requirement}
</Text>
))}
</View>
)}
{/* Confirm Password Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Confirm New Password *</Text>
<View style={styles.passwordInputContainer}>
<TextInput
style={[
styles.textInput,
styles.passwordInput,
errors.confirmPassword ? styles.inputError : null,
]}
value={formData.confirmPassword}
onChangeText={(value) => handleInputChange('confirmPassword', value)}
onBlur={() => handleInputBlur('confirmPassword')}
placeholder="Confirm your new password"
placeholderTextColor={theme.colors.textMuted}
secureTextEntry={!showConfirmPassword}
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.eyeIcon}
onPress={() => togglePasswordVisibility('confirm')}
activeOpacity={0.7}
>
<Icon
name={showConfirmPassword ? 'eye-off' : 'eye'}
size={20}
color={theme.colors.textMuted}
/>
</TouchableOpacity>
</View>
{errors.confirmPassword && (
<Text style={styles.errorText}>{errors.confirmPassword}</Text>
)}
</View>
</View>
{/* Action buttons */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[
styles.submitButton,
isSubmitting && styles.submitButtonDisabled,
]}
onPress={handleSubmit}
disabled={isSubmitting}
activeOpacity={0.7}
>
<Text style={[
styles.submitButtonText,
isSubmitting && styles.submitButtonTextDisabled,
]}>
{isSubmitting ? 'Changing Password...' : 'Change Password'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.cancelButton}
onPress={handleCancel}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
</View>
{/* Bottom spacing for tab bar */}
<View style={styles.bottomSpacing} />
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES SECTION
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view styling
scrollView: {
flex: 1,
},
// Scroll content styling
scrollContent: {
paddingHorizontal: theme.spacing.md,
},
// Bottom spacing for tab bar
bottomSpacing: {
height: theme.spacing.xl,
},
// Information sections
infoSection: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.md,
},
infoText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
lineHeight: 22,
marginBottom: theme.spacing.md,
},
// Requirements list styling
requirementsList: {
marginTop: theme.spacing.sm,
},
requirementItem: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Input container styling
inputContainer: {
marginBottom: theme.spacing.sm,
backgroundColor: theme.colors.background,
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.small,
},
inputLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
fontWeight: '600',
},
// Password input styling
passwordInputContainer: {
position: 'relative',
},
textInput: {
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md,
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
backgroundColor: theme.colors.background,
minHeight: 48,
},
passwordInput: {
paddingRight: theme.spacing.xl + theme.spacing.md,
},
eyeIcon: {
position: 'absolute',
right: theme.spacing.md,
top: theme.spacing.md,
padding: theme.spacing.xs,
},
inputError: {
borderColor: theme.colors.error,
backgroundColor: theme.colors.error + '10',
},
errorText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.error,
marginTop: theme.spacing.sm,
marginLeft: theme.spacing.xs,
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
backgroundColor: theme.colors.error + '10',
borderRadius: theme.borderRadius.small,
alignSelf: 'flex-start',
},
// Password strength styling
strengthContainer: {
marginBottom: theme.spacing.md,
},
strengthLabel: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
strengthBar: {
height: 4,
backgroundColor: theme.colors.border,
borderRadius: 2,
marginBottom: theme.spacing.xs,
overflow: 'hidden',
},
strengthProgress: {
height: '100%',
borderRadius: 2,
},
strengthText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
textAlign: 'center',
},
// Requirements check styling
requirementsCheck: {
marginBottom: theme.spacing.md,
padding: theme.spacing.sm,
backgroundColor: theme.colors.backgroundAlt,
borderRadius: theme.borderRadius.small,
},
requirementCheckItem: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
// Button container
buttonContainer: {
marginTop: theme.spacing.lg,
marginBottom: theme.spacing.md,
},
submitButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
submitButtonDisabled: {
backgroundColor: theme.colors.border,
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
submitButtonText: {
color: theme.colors.background,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
},
submitButtonTextDisabled: {
color: theme.colors.textMuted,
},
cancelButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
},
cancelButtonText: {
color: theme.colors.textSecondary,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
},
});
/*
* End of File: ChangePasswordScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -0,0 +1,637 @@
/*
* File: EditProfileScreen.tsx
* Description: Edit profile screen for updating user information
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TextInput,
TouchableOpacity,
Alert,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import Icon from 'react-native-vector-icons/Feather';
import { theme } from '../../../theme/theme';
import { SettingsHeader } from '../components/SettingsHeader';
import { useAppSelector, useAppDispatch } from '../../../store/hooks';
import {
selectUserFirstName,
selectUserLastName,
selectUserDisplayName,
selectUserEmail,
selectUser,
} from '../../Auth/redux/authSelectors';
import { updateUserProfileAsync } from '../../Auth/redux/authActions';
/**
* EditProfileScreenProps Interface
*
* Purpose: Defines the props required by the EditProfileScreen component
*
* Props:
* - navigation: React Navigation object for screen navigation
*/
interface EditProfileScreenProps {
navigation: any;
}
/**
* FormData Interface
*
* Purpose: Defines the structure of the profile form data
*/
interface FormData {
firstName: string;
lastName: string;
displayName: string;
}
/**
* FormErrors Interface
*
* Purpose: Defines the structure of form validation errors
*/
interface FormErrors {
firstName?: string;
lastName?: string;
displayName?: string;
}
/**
* EditProfileScreen Component
*
* Purpose: Allows users to edit their profile information including first name,
* last name, and display name with proper validation
*
* Features:
* 1. Pre-populated form with current user data
* 2. Real-time validation
* 3. Form submission with error handling
* 4. Clean and intuitive user interface
*/
export const EditProfileScreen: React.FC<EditProfileScreenProps> = ({
navigation,
}) => {
// ============================================================================
// REDUX STATE
// ============================================================================
const dispatch = useAppDispatch();
const user = useAppSelector(selectUser);
const currentFirstName = useAppSelector(selectUserFirstName);
const currentLastName = useAppSelector(selectUserLastName);
const currentDisplayName = useAppSelector(selectUserDisplayName);
const currentEmail = useAppSelector(selectUserEmail);
// ============================================================================
// LOCAL STATE
// ============================================================================
const [formData, setFormData] = useState<FormData>({
firstName: '',
lastName: '',
displayName: '',
});
const [errors, setErrors] = useState<FormErrors>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
// ============================================================================
// EFFECTS
// ============================================================================
/**
* Initialize form data with current user information
*/
useEffect(() => {
setFormData({
firstName: currentFirstName || '',
lastName: currentLastName || '',
displayName: currentDisplayName || '',
});
}, [currentFirstName, currentLastName, currentDisplayName]);
/**
* Check if form has unsaved changes
*/
useEffect(() => {
const hasUnsavedChanges =
formData.firstName !== currentFirstName ||
formData.lastName !== currentLastName ||
formData.displayName !== currentDisplayName;
setHasChanges(hasUnsavedChanges);
}, [formData, currentFirstName, currentLastName, currentDisplayName]);
// ============================================================================
// VALIDATION FUNCTIONS
// ============================================================================
/**
* validateField Function
*
* Purpose: Validate individual form fields
*
* @param field - Field name to validate
* @param value - Field value to validate
* @returns Validation error message or undefined
*/
const validateField = (field: keyof FormData, value: string): string | undefined => {
switch (field) {
case 'firstName':
if (!value.trim()) {
return 'First name is required';
}
if (value.trim().length < 2) {
return 'First name must be at least 2 characters';
}
if (value.trim().length > 50) {
return 'First name must be less than 50 characters';
}
if (!/^[a-zA-Z\s'-]+$/.test(value.trim())) {
return 'First name can only contain letters, spaces, hyphens, and apostrophes';
}
break;
case 'lastName':
if (!value.trim()) {
return 'Last name is required';
}
if (value.trim().length < 1) {
return 'Last name must be at least 1 character';
}
if (value.trim().length > 50) {
return 'Last name must be less than 50 characters';
}
if (!/^[a-zA-Z\s'-]+$/.test(value.trim())) {
return 'Last name can only contain letters, spaces, hyphens, and apostrophes';
}
break;
case 'displayName':
if (!value.trim()) {
return 'Display name is required';
}
if (value.trim().length < 2) {
return 'Display name must be at least 2 characters';
}
if (value.trim().length > 30) {
return 'Display name must be less than 30 characters';
}
break;
}
return undefined;
};
/**
* validateForm Function
*
* Purpose: Validate entire form and return validation errors
*
* @returns Object containing validation errors
*/
const validateForm = (): FormErrors => {
const newErrors: FormErrors = {};
Object.keys(formData).forEach((field) => {
const key = field as keyof FormData;
const error = validateField(key, formData[key]);
if (error) {
newErrors[key] = error;
}
});
return newErrors;
};
// ============================================================================
// EVENT HANDLERS
// ============================================================================
/**
* handleInputChange Function
*
* Purpose: Handle input field changes and clear field-specific errors
*
* @param field - Field name that changed
* @param value - New field value
*/
const handleInputChange = (field: keyof FormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Clear field-specific error when user starts typing
if (errors[field]) {
setErrors(prev => ({ ...prev, [field]: undefined }));
}
};
/**
* handleInputBlur Function
*
* Purpose: Validate field when user leaves the input
*
* @param field - Field name to validate
*/
const handleInputBlur = (field: keyof FormData) => {
const error = validateField(field, formData[field]);
setErrors(prev => ({ ...prev, [field]: error }));
};
/**
* handleSubmit Function
*
* Purpose: Handle form submission with validation and API call
*/
const handleSubmit = async () => {
// Validate form
const validationErrors = validateForm();
if (Object.keys(validationErrors).length > 0) {
setErrors(validationErrors);
return;
}
setIsSubmitting(true);
try {
// Dispatch update action
await dispatch(updateUserProfileAsync({
first_name: formData.firstName.trim(),
last_name: formData.lastName.trim(),
})).unwrap();
// Navigate back after successful profile update
navigation.goBack();
} catch (error: any) {
// Handle error - toast notification is already shown in the thunk
console.error('Profile update error:', error);
} finally {
setIsSubmitting(false);
}
};
/**
* handleCancel Function
*
* Purpose: Handle cancel action with unsaved changes warning
*/
const handleCancel = () => {
if (hasChanges) {
Alert.alert(
'Unsaved Changes',
'You have unsaved changes. Are you sure you want to leave?',
[
{
text: 'Cancel',
style: 'cancel',
},
{
text: 'Leave',
style: 'destructive',
onPress: () => navigation.goBack(),
},
]
);
} else {
navigation.goBack();
}
};
// ============================================================================
// MAIN RENDER
// ============================================================================
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
{/* Header with back button */}
<SettingsHeader
title="Edit Profile"
showBackButton={true}
onBackPress={handleCancel}
/>
{/* Scrollable form content */}
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
{/* Current email display (read-only) */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Account Information</Text>
<View style={styles.readOnlyField}>
<Text style={styles.readOnlyLabel}>Email Address</Text>
<View style={styles.emailContainer}>
<Text style={styles.readOnlyValue}>{currentEmail}</Text>
<View style={styles.lockIcon}>
<Icon name="lock" size={16} color={theme.colors.textMuted} />
</View>
</View>
<Text style={styles.readOnlyHint}>Email address cannot be changed</Text>
</View>
</View>
{/* Profile form section */}
<View style={styles.infoSection}>
<Text style={styles.sectionTitle}>Personal Information</Text>
{/* First Name Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>First Name *</Text>
<TextInput
style={[
styles.textInput,
errors.firstName ? styles.inputError : null,
]}
value={formData.firstName}
onChangeText={(value) => handleInputChange('firstName', value)}
onBlur={() => handleInputBlur('firstName')}
placeholder="Enter your first name"
placeholderTextColor={theme.colors.textMuted}
autoCapitalize="words"
autoCorrect={false}
maxLength={50}
/>
{errors.firstName && (
<Text style={styles.errorText}>{errors.firstName}</Text>
)}
</View>
{/* Last Name Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Last Name *</Text>
<TextInput
style={[
styles.textInput,
errors.lastName ? styles.inputError : null,
]}
value={formData.lastName}
onChangeText={(value) => handleInputChange('lastName', value)}
onBlur={() => handleInputBlur('lastName')}
placeholder="Enter your last name"
placeholderTextColor={theme.colors.textMuted}
autoCapitalize="words"
autoCorrect={false}
maxLength={50}
/>
{errors.lastName && (
<Text style={styles.errorText}>{errors.lastName}</Text>
)}
</View>
{/* Display Name Input */}
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>Display Name *</Text>
<TextInput
style={[
styles.textInput,
errors.displayName ? styles.inputError : null,
]}
value={formData.displayName}
onChangeText={(value) => handleInputChange('displayName', value)}
onBlur={() => handleInputBlur('displayName')}
placeholder="Enter your display name"
placeholderTextColor={theme.colors.textMuted}
autoCapitalize="words"
autoCorrect={false}
maxLength={30}
/>
{errors.displayName && (
<Text style={styles.errorText}>{errors.displayName}</Text>
)}
</View>
</View>
{/* Action buttons */}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[
styles.submitButton,
(!hasChanges || isSubmitting) && styles.submitButtonDisabled,
]}
onPress={handleSubmit}
disabled={!hasChanges || isSubmitting}
activeOpacity={0.7}
>
<Text style={[
styles.submitButtonText,
(!hasChanges || isSubmitting) && styles.submitButtonTextDisabled,
]}>
{isSubmitting ? 'Updating...' : 'Update Profile'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.cancelButton}
onPress={handleCancel}
activeOpacity={0.7}
>
<Text style={styles.cancelButtonText}>Cancel</Text>
</TouchableOpacity>
</View>
{/* Bottom spacing for tab bar */}
<View style={styles.bottomSpacing} />
</ScrollView>
</KeyboardAvoidingView>
);
};
// ============================================================================
// STYLES SECTION
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Scroll view styling
scrollView: {
flex: 1,
},
// Scroll content styling
scrollContent: {
paddingHorizontal: theme.spacing.md,
},
// Bottom spacing for tab bar
bottomSpacing: {
height: theme.spacing.xl,
},
// Information sections
infoSection: {
backgroundColor: theme.colors.background,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.md,
},
// Read-only field styling
readOnlyField: {
paddingVertical: theme.spacing.sm,
borderBottomColor: theme.colors.border,
borderBottomWidth: 1,
},
readOnlyLabel: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textMuted,
marginBottom: theme.spacing.xs,
},
emailContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: theme.spacing.xs,
},
readOnlyValue: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
flex: 1,
},
lockIcon: {
padding: theme.spacing.xs,
backgroundColor: theme.colors.backgroundAlt,
borderRadius: theme.borderRadius.small,
},
readOnlyHint: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textMuted,
fontStyle: 'italic',
},
// Input container styling
inputContainer: {
marginBottom: theme.spacing.sm,
backgroundColor: theme.colors.background,
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.small,
},
inputLabel: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
fontWeight: '600',
},
textInput: {
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
paddingVertical: theme.spacing.md,
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
backgroundColor: theme.colors.background,
minHeight: 48,
},
inputError: {
borderColor: theme.colors.error,
backgroundColor: theme.colors.error + '10',
},
errorText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.error,
marginTop: theme.spacing.sm,
marginLeft: theme.spacing.xs,
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
backgroundColor: theme.colors.error + '10',
borderRadius: theme.borderRadius.small,
alignSelf: 'flex-start',
},
// Button container
buttonContainer: {
marginTop: theme.spacing.lg,
marginBottom: theme.spacing.md,
},
submitButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
marginBottom: theme.spacing.md,
...theme.shadows.primary,
},
submitButtonDisabled: {
backgroundColor: theme.colors.border,
shadowColor: 'transparent',
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0,
shadowRadius: 0,
elevation: 0,
},
submitButtonText: {
color: theme.colors.background,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
},
submitButtonTextDisabled: {
color: theme.colors.textMuted,
},
cancelButton: {
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
},
cancelButtonText: {
color: theme.colors.textSecondary,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
},
});
/*
* End of File: EditProfileScreen.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -11,7 +11,6 @@ import {
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
RefreshControl,
Image,
@ -35,7 +34,6 @@ import {
selectUserFirstName,
selectUserLastName,
selectUserProfilePhoto,
selectNotificationPreferences,
selectDashboardSettings
} from '../../Auth/redux/authSelectors';
@ -106,7 +104,6 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
const userFirstName = useAppSelector(selectUserFirstName);
const userLastName = useAppSelector(selectUserLastName);
const userProfilePhoto = useAppSelector(selectUserProfilePhoto);
const notificationPreferences = useAppSelector(selectNotificationPreferences);
const dashboardSettings = useAppSelector(selectDashboardSettings);
@ -142,79 +139,10 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
type: 'NAVIGATION',
onPress: () => handleNavigation('CHANGE_PASSWORD'),
},
{
id: 'security-settings',
title: 'Security Settings',
subtitle: 'Two-factor authentication and biometrics',
icon: 'shield',
type: 'NAVIGATION',
onPress: () => handleNavigation('SECURITY'),
},
],
},
{
id: 'NOTIFICATIONS',
title: 'Notifications',
items: [
{
id: 'notification-preferences',
title: 'Notification Preferences',
subtitle: 'Manage alert and notification settings',
icon: 'bell',
type: 'NAVIGATION',
onPress: () => handleNavigation('NOTIFICATIONS'),
},
{
id: 'quiet-hours',
title: 'Quiet Hours',
subtitle: 'Set do not disturb periods',
icon: 'moon',
type: 'NAVIGATION',
onPress: () => handleNavigation('QUIET_HOURS'),
},
{
id: 'push-notifications',
title: 'Push Notifications',
subtitle: 'Enable or disable push notifications',
icon: 'phone',
type: 'TOGGLE',
value: notificationPreferences?.system_notifications.push,
onPress: () => handleToggleSetting('pushNotifications'),
},
],
},
{
id: 'PRIVACY',
title: 'Privacy & Security',
items: [
{
id: 'privacy-settings',
title: 'Privacy Settings',
subtitle: 'Manage data sharing and privacy controls',
icon: 'settings',
type: 'NAVIGATION',
onPress: () => handleNavigation('PRIVACY'),
},
{
id: 'biometric-auth',
title: 'Biometric Authentication',
subtitle: 'Use fingerprint or face ID',
icon: 'lock',
type: 'TOGGLE',
value: false, // TODO: Add biometric auth preference to user data
onPress: () => handleToggleSetting('biometricAuth'),
},
{
id: 'session-timeout',
title: 'Session Timeout',
subtitle: 'Auto-logout after inactivity',
icon: 'clock',
type: 'NAVIGATION',
onPress: () => handleNavigation('SESSION_TIMEOUT'),
},
],
},
{
id: 'ABOUT',
@ -236,14 +164,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
type: 'NAVIGATION',
onPress: () => handleNavigation('HELP'),
},
{
id: 'feedback',
title: 'Send Feedback',
subtitle: 'Report bugs or suggest improvements',
icon: 'rss',
type: 'NAVIGATION',
onPress: () => handleNavigation('FEEDBACK'),
},
],
},
{
@ -273,7 +194,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
*/
useEffect(() => {
setSettingsSections(generateSettingsSections());
}, [user, notificationPreferences, dashboardSettings]);
}, [user, dashboardSettings]);
// ============================================================================
// EVENT HANDLERS
@ -310,17 +231,40 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
* @param screen - Screen to navigate to
*/
const handleNavigation = (screen: string) => {
// TODO: Implement navigation to specific settings screens
console.log('Navigate to:', screen);
setModalConfig({
title: 'Navigation',
message: `Navigate to ${screen} screen`,
type: 'info',
onConfirm: () => {},
showCancel: false,
icon: 'info',
});
setModalVisible(true);
switch (screen) {
case 'APP_INFO':
navigation.navigate('AppInfoScreen');
break;
case 'PROFILE':
navigation.navigate('EditProfileScreen');
break;
case 'CHANGE_PASSWORD':
navigation.navigate('ChangePasswordScreen');
break;
case 'HELP':
// TODO: Implement help and support
setModalConfig({
title: 'Help & Support',
message: 'Help and support functionality coming soon!',
type: 'info',
onConfirm: () => {},
showCancel: false,
icon: 'info',
});
setModalVisible(true);
break;
default:
console.log('Navigate to:', screen);
setModalConfig({
title: 'Navigation',
message: `Navigate to ${screen} screen`,
type: 'info',
onConfirm: () => {},
showCancel: false,
icon: 'info',
});
setModalVisible(true);
}
};
/**
@ -387,14 +331,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
setModalVisible(true);
};
/**
* handleProfilePress Function
*
* Purpose: Handle profile card press navigation
*/
const handleProfilePress = () => {
handleNavigation('PROFILE');
};
// ============================================================================
// MAIN RENDER
@ -422,37 +359,31 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
{/* Profile card section */}
{user && (
<View style={styles.profileCard}>
<TouchableOpacity onPress={handleProfilePress} activeOpacity={0.7}>
<View style={styles.profileHeader}>
<View style={styles.profileImageContainer}>
{user.profile_photo_url ? (
<Image
source={{ uri: user.profile_photo_url }}
style={styles.profileImage}
resizeMode="cover"
/>
) : (
<View style={styles.fallbackAvatar}>
<Text style={styles.fallbackText}>
{user.first_name.charAt(0)}{user.last_name.charAt(0)}
</Text>
</View>
)}
</View>
<View style={styles.profileInfo}>
<Text style={styles.profileName}>
{user.display_name || `${user.first_name} ${user.last_name}`}
</Text>
<Text style={styles.profileEmail}>{user.email}</Text>
<Text style={styles.profileRole}>Physician</Text>
</View>
<View style={styles.editIcon}>
<Text style={styles.editText}>Edit</Text>
</View>
<View style={styles.profileHeader}>
<View style={styles.profileImageContainer}>
{user.profile_photo_url ? (
<Image
source={{ uri: user.profile_photo_url }}
style={styles.profileImage}
resizeMode="cover"
/>
) : (
<View style={styles.fallbackAvatar}>
<Text style={styles.fallbackText}>
{user.first_name.charAt(0)}{user.last_name.charAt(0)}
</Text>
</View>
)}
</View>
</TouchableOpacity>
<View style={styles.profileInfo}>
<Text style={styles.profileName}>
{user.display_name || `${user.first_name} ${user.last_name}`}
</Text>
<Text style={styles.profileEmail}>{user.email}</Text>
<Text style={styles.profileRole}>Radiologist</Text>
</View>
</View>
</View>
)}
@ -589,18 +520,7 @@ const styles = StyleSheet.create({
color: theme.colors.primary,
},
editIcon: {
paddingHorizontal: theme.spacing.sm,
paddingVertical: theme.spacing.xs,
backgroundColor: theme.colors.backgroundAlt,
borderRadius: theme.borderRadius.small,
},
editText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.primary,
},
});
/*

View File

@ -0,0 +1,17 @@
/*
* File: index.ts
* Description: Export all screen components from Settings module
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
export { SettingsScreen } from './SettingsScreen';
export { AppInfoScreen } from './AppInfoScreen';
export { EditProfileScreen } from './EditProfileScreen';
export { ChangePasswordScreen } from './ChangePasswordScreen';
/*
* End of File: index.ts
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/

View File

@ -99,7 +99,7 @@ export const MainTabNavigator: React.FC = () => {
/>
{/* AI Predictions Tab - AI-powered medical predictions */}
<Tab.Screen
{/* <Tab.Screen
name="AIPredictions"
component={AIPredictionStackNavigator}
options={{
@ -110,7 +110,7 @@ export const MainTabNavigator: React.FC = () => {
),
headerShown: false,
}}
/>
/> */}
{/* Reports Tab - Medical documentation */}
{/* <Tab.Screen

View File

@ -96,8 +96,22 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal
const timer = setTimeout(() => {
if (webViewRef.current) {
try {
// Send the URL directly as a string message
webViewRef.current.postMessage(dicomUrl);
debugLog('DICOM URL sent successfully');
// Also try sending as a structured message
setTimeout(() => {
if (webViewRef.current) {
const structuredMessage = JSON.stringify({
type: 'loadDicom',
data: dicomUrl
});
webViewRef.current.postMessage(structuredMessage);
debugLog('Structured DICOM message sent');
}
}, 500);
} catch (error) {
debugLog(`Failed to send DICOM URL: ${error}`);
}