dashboard added and profile update
This commit is contained in:
parent
692a8156da
commit
467dc0b8cf
@ -29,7 +29,7 @@ NeoScan_Physician/
|
|||||||
│ │ │ │ ├── QuickActions.tsx # Emergency quick actions
|
│ │ │ │ ├── QuickActions.tsx # Emergency quick actions
|
||||||
│ │ │ │ └── DepartmentStats.tsx # Department statistics
|
│ │ │ │ └── DepartmentStats.tsx # Department statistics
|
||||||
│ │ │ ├── screens/ # Dashboard screens
|
│ │ │ ├── screens/ # Dashboard screens
|
||||||
│ │ │ │ └── ERDashboardScreen.tsx # Main ER dashboard
|
│ │ │ │ └── DashboardScreen.tsx # Main ER dashboard
|
||||||
│ │ │ ├── hooks/ # Dashboard custom hooks
|
│ │ │ ├── hooks/ # Dashboard custom hooks
|
||||||
│ │ │ ├── redux/ # Dashboard state management
|
│ │ │ ├── redux/ # Dashboard state management
|
||||||
│ │ │ ├── services/ # Dashboard API services
|
│ │ │ ├── services/ # Dashboard API services
|
||||||
@ -223,7 +223,7 @@ NeoScan_Physician/
|
|||||||
|
|
||||||
### Dashboard Module
|
### Dashboard Module
|
||||||
**Purpose**: Main ER dashboard with patient monitoring and alerts
|
**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
|
- **PatientCard**: Individual patient information display
|
||||||
- **CriticalAlerts**: High-priority alert notifications
|
- **CriticalAlerts**: High-priority alert notifications
|
||||||
- **QuickActions**: Emergency procedure shortcuts
|
- **QuickActions**: Emergency procedure shortcuts
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">NeoScanPhysician</string>
|
<string name="app_name">Radiologist</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -152,7 +152,12 @@ const AIPredictionStackNavigator: React.FC = () => {
|
|||||||
{/* AI Prediction Details Screen */}
|
{/* AI Prediction Details Screen */}
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="AIPredictionDetails"
|
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 }) => ({
|
options={({ navigation, route }) => ({
|
||||||
title: 'Create Suggestion',
|
title: 'Create Suggestion',
|
||||||
headerLeft: () => (
|
headerLeft: () => (
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import { logout } from './authSlice';
|
import { logout, updateUserProfile } from './authSlice';
|
||||||
import { authAPI } from '../services/authAPI';
|
import { authAPI } from '../services/authAPI';
|
||||||
import { showError, showSuccess } from '../../../shared/utils/toast';
|
import { showError, showSuccess } from '../../../shared/utils/toast';
|
||||||
|
|
||||||
@ -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
|
* Thunk to logout user
|
||||||
|
|||||||
@ -138,8 +138,8 @@ const LoginScreen: React.FC<LoginScreenProps> = ({ navigation }) => {
|
|||||||
* HEADER SECTION - App branding and title
|
* HEADER SECTION - App branding and title
|
||||||
* ======================================================================== */}
|
* ======================================================================== */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.title}>Physician</Text>
|
<Text style={styles.title}>Radiologist</Text>
|
||||||
<Text style={styles.subtitle}>Emergency Department Access</Text>
|
{/* <Text style={styles.subtitle}>Emergency Department Access</Text> */}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.imageContainer}>
|
<View style={styles.imageContainer}>
|
||||||
<Image source={require('../../../assets/images/hospital-logo.png')} style={styles.image} />
|
<Image source={require('../../../assets/images/hospital-logo.png')} style={styles.image} />
|
||||||
|
|||||||
@ -226,7 +226,7 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ navigation }) => {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let role = 'er_physician';
|
let role = 'radiologist';
|
||||||
|
|
||||||
// Prepare form data with proper file handling
|
// Prepare form data with proper file handling
|
||||||
const formFields = {
|
const formFields = {
|
||||||
|
|||||||
@ -38,7 +38,27 @@ export const authAPI = {
|
|||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
...(token && { 'Authorization': `Bearer ${token}` }),
|
...(token && { 'Authorization': `Bearer ${token}` }),
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
|
||||||
|
// Update user profile
|
||||||
|
updateUserProfile: (userId: string, profileData: {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
}, token: string) => api.put(
|
||||||
|
`/api/auth/auth/admin/users/self/${userId}`,
|
||||||
|
profileData,
|
||||||
|
buildHeaders({ token })
|
||||||
|
),
|
||||||
|
|
||||||
|
// Change password (admin endpoint)
|
||||||
|
changePassword: (userId: string, passwordData: {
|
||||||
|
password: string;
|
||||||
|
}, token: string) => api.put(
|
||||||
|
`/api/auth/auth/admin/users/self/${userId}`,
|
||||||
|
passwordData,
|
||||||
|
buildHeaders({ token })
|
||||||
|
),
|
||||||
|
|
||||||
// Add more endpoints as needed
|
// Add more endpoints as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -29,33 +29,6 @@ export const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
|||||||
{dashboard.shiftInfo.currentShift} Shift • {dashboard.shiftInfo.attendingPhysician}
|
{dashboard.shiftInfo.currentShift} Shift • {dashboard.shiftInfo.attendingPhysician}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.statsContainer}>
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<Text style={styles.statValue}>{dashboard.totalPatients}</Text>
|
|
||||||
<Text style={styles.statLabel}>Total Patients</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<Text style={[styles.statValue, styles.criticalValue]}>
|
|
||||||
{dashboard.criticalPatients}
|
|
||||||
</Text>
|
|
||||||
<Text style={styles.statLabel}>Critical</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<Text style={styles.statValue}>{dashboard.pendingScans}</Text>
|
|
||||||
<Text style={styles.statLabel}>Pending Scans</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<Text style={styles.statValue}>{dashboard.bedOccupancy}%</Text>
|
|
||||||
<Text style={styles.statLabel}>Bed Occupancy</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.lastUpdated}>
|
|
||||||
<Text style={styles.lastUpdatedText}>
|
|
||||||
Last updated: {dashboard.lastUpdated.toLocaleTimeString()}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -68,9 +41,6 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: theme.spacing.lg,
|
marginBottom: theme.spacing.lg,
|
||||||
...theme.shadows.medium,
|
...theme.shadows.medium,
|
||||||
},
|
},
|
||||||
header: {
|
|
||||||
marginBottom: theme.spacing.lg,
|
|
||||||
},
|
|
||||||
title: {
|
title: {
|
||||||
fontSize: theme.typography.fontSize.displayMedium,
|
fontSize: theme.typography.fontSize.displayMedium,
|
||||||
fontFamily: theme.typography.fontFamily.bold,
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
@ -81,36 +51,6 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: theme.typography.fontSize.bodyMedium,
|
fontSize: theme.typography.fontSize.bodyMedium,
|
||||||
color: theme.colors.textSecondary,
|
color: theme.colors.textSecondary,
|
||||||
},
|
},
|
||||||
statsContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginBottom: theme.spacing.lg,
|
|
||||||
},
|
|
||||||
statItem: {
|
|
||||||
alignItems: 'center',
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
statValue: {
|
|
||||||
fontSize: theme.typography.fontSize.displaySmall,
|
|
||||||
fontFamily: theme.typography.fontFamily.bold,
|
|
||||||
color: theme.colors.primary,
|
|
||||||
marginBottom: theme.spacing.xs,
|
|
||||||
},
|
|
||||||
criticalValue: {
|
|
||||||
color: theme.colors.critical,
|
|
||||||
},
|
|
||||||
statLabel: {
|
|
||||||
fontSize: theme.typography.fontSize.bodySmall,
|
|
||||||
color: theme.colors.textSecondary,
|
|
||||||
textAlign: 'center',
|
|
||||||
},
|
|
||||||
lastUpdated: {
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
lastUpdatedText: {
|
|
||||||
fontSize: theme.typography.fontSize.caption,
|
|
||||||
color: theme.colors.textMuted,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Export screens
|
// Export screens
|
||||||
export { default as ERDashboardScreen } from './screens/ERDashboardScreen';
|
export { default as DashboardScreen } from './screens/DashboardScreen';
|
||||||
|
|
||||||
// Export navigation
|
// Export navigation
|
||||||
export {
|
export {
|
||||||
@ -14,7 +14,7 @@ export {
|
|||||||
DashboardStackParamList,
|
DashboardStackParamList,
|
||||||
DashboardNavigationProp,
|
DashboardNavigationProp,
|
||||||
DashboardScreenProps,
|
DashboardScreenProps,
|
||||||
ERDashboardScreenProps,
|
DashboardScreenProps,
|
||||||
PatientDetailsScreenProps,
|
PatientDetailsScreenProps,
|
||||||
AlertDetailsScreenProps,
|
AlertDetailsScreenProps,
|
||||||
DepartmentStatsScreenProps,
|
DepartmentStatsScreenProps,
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import React from 'react';
|
|||||||
import { createStackNavigator } from '@react-navigation/stack';
|
import { createStackNavigator } from '@react-navigation/stack';
|
||||||
|
|
||||||
// Import dashboard screens
|
// Import dashboard screens
|
||||||
import { ERDashboardScreen } from '../screens/ERDashboardScreen';
|
import { DashboardScreen } from '../screens/DashboardScreen';
|
||||||
|
|
||||||
// Import navigation types
|
// Import navigation types
|
||||||
import { DashboardStackParamList } from './navigationTypes';
|
import { DashboardStackParamList } from './navigationTypes';
|
||||||
@ -22,7 +22,7 @@ const Stack = createStackNavigator<DashboardStackParamList>();
|
|||||||
* DashboardStackNavigator - Manages navigation between dashboard screens
|
* DashboardStackNavigator - Manages navigation between dashboard screens
|
||||||
*
|
*
|
||||||
* This navigator handles the flow between:
|
* This navigator handles the flow between:
|
||||||
* - ERDashboardScreen: Main ER dashboard with patient overview
|
* - DashboardScreen: Main ER dashboard with patient overview
|
||||||
* - Future screens: Patient details, alerts, reports, etc.
|
* - Future screens: Patient details, alerts, reports, etc.
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
@ -72,7 +72,7 @@ const DashboardStackNavigator: React.FC = () => {
|
|||||||
{/* ER Dashboard Screen - Main dashboard entry point */}
|
{/* ER Dashboard Screen - Main dashboard entry point */}
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
name="ERDashboard"
|
name="ERDashboard"
|
||||||
component={ERDashboardScreen}
|
component={DashboardScreen}
|
||||||
options={{
|
options={{
|
||||||
title: 'ER Dashboard',
|
title: 'ER Dashboard',
|
||||||
headerShown: false, // Hide header for main dashboard
|
headerShown: false, // Hide header for main dashboard
|
||||||
|
|||||||
@ -13,12 +13,12 @@ export type {
|
|||||||
DashboardStackParamList,
|
DashboardStackParamList,
|
||||||
DashboardNavigationProp,
|
DashboardNavigationProp,
|
||||||
DashboardScreenProps,
|
DashboardScreenProps,
|
||||||
ERDashboardScreenProps,
|
DashboardScreenProps,
|
||||||
PatientDetailsScreenProps,
|
PatientDetailsScreenProps,
|
||||||
AlertDetailsScreenProps,
|
AlertDetailsScreenProps,
|
||||||
DepartmentStatsScreenProps,
|
DepartmentStatsScreenProps,
|
||||||
QuickActionsScreenProps,
|
QuickActionsScreenProps,
|
||||||
ERDashboardScreenParams,
|
DashboardScreenParams,
|
||||||
PatientDetailsScreenParams,
|
PatientDetailsScreenParams,
|
||||||
AlertDetailsScreenParams,
|
AlertDetailsScreenParams,
|
||||||
DepartmentStatsScreenParams,
|
DepartmentStatsScreenParams,
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { Patient, Alert as AlertType, ERDashboard } from '../../../shared/types'
|
|||||||
*/
|
*/
|
||||||
export type DashboardStackParamList = {
|
export type DashboardStackParamList = {
|
||||||
// ER Dashboard screen - Main dashboard with patient overview
|
// ER Dashboard screen - Main dashboard with patient overview
|
||||||
ERDashboard: ERDashboardScreenParams;
|
ERDashboard: DashboardScreenParams;
|
||||||
|
|
||||||
// Patient Details screen - Detailed patient information
|
// Patient Details screen - Detailed patient information
|
||||||
PatientDetails: PatientDetailsScreenParams;
|
PatientDetails: PatientDetailsScreenParams;
|
||||||
@ -59,7 +59,7 @@ export interface DashboardScreenProps<T extends keyof DashboardStackParamList> {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ERDashboardScreenParams
|
* DashboardScreenParams
|
||||||
*
|
*
|
||||||
* Purpose: Parameters passed to the ER dashboard screen
|
* Purpose: Parameters passed to the ER dashboard screen
|
||||||
*
|
*
|
||||||
@ -67,7 +67,7 @@ export interface DashboardScreenProps<T extends keyof DashboardStackParamList> {
|
|||||||
* - filter: Optional filter to apply to dashboard data
|
* - filter: Optional filter to apply to dashboard data
|
||||||
* - refresh: Optional flag to force refresh
|
* - refresh: Optional flag to force refresh
|
||||||
*/
|
*/
|
||||||
export interface ERDashboardScreenParams {
|
export interface DashboardScreenParams {
|
||||||
filter?: 'all' | 'critical' | 'active' | 'pending';
|
filter?: 'all' | 'critical' | 'active' | 'pending';
|
||||||
refresh?: boolean;
|
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
|
* PatientDetailsScreenProps - Props for PatientDetailsScreen component
|
||||||
|
|||||||
811
app/modules/Dashboard/screens/DashboardScreen.tsx
Normal file
811
app/modules/Dashboard/screens/DashboardScreen.tsx
Normal 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.
|
||||||
|
*/
|
||||||
@ -21,7 +21,6 @@ import { ERDashboard, Patient, Alert as AlertType } from '../../../shared/types'
|
|||||||
import { PatientCard } from '../components/PatientCard';
|
import { PatientCard } from '../components/PatientCard';
|
||||||
import { CriticalAlerts } from '../components/CriticalAlerts';
|
import { CriticalAlerts } from '../components/CriticalAlerts';
|
||||||
import { DashboardHeader } from '../components/DashboardHeader';
|
import { DashboardHeader } from '../components/DashboardHeader';
|
||||||
import { QuickActions } from '../components/QuickActions';
|
|
||||||
import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview';
|
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 */}
|
{/* Department statistics showing brain case distribution */}
|
||||||
{dashboard && <BrainPredictionsOverview dashboard={dashboard} />}
|
{dashboard && <BrainPredictionsOverview dashboard={dashboard} />}
|
||||||
|
|
||||||
|
|||||||
57
app/modules/Dashboard/screens/patient.json
Normal file
57
app/modules/Dashboard/screens/patient.json
Normal 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"
|
||||||
|
}
|
||||||
@ -21,18 +21,18 @@ import Icon from 'react-native-vector-icons/Feather';
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
interface FilterTabsProps {
|
interface FilterTabsProps {
|
||||||
selectedFilter: 'all' | 'Critical' | 'Routine' | 'Emergency';
|
selectedFilter: 'all' | 'processed' | 'pending' | 'error';
|
||||||
onFilterChange: (filter: 'all' | 'Critical' | 'Routine' | 'Emergency') => void;
|
onFilterChange: (filter: 'all' | 'processed' | 'pending' | 'error') => void;
|
||||||
patientCounts: {
|
patientCounts: {
|
||||||
all: number;
|
all: number;
|
||||||
Critical: number;
|
processed: number;
|
||||||
Routine: number;
|
pending: number;
|
||||||
Emergency: number;
|
error: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterTab {
|
interface FilterTab {
|
||||||
id: 'all' | 'Critical' | 'Routine' | 'Emergency';
|
id: 'all' | 'processed' | 'pending' | 'error';
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
color: string;
|
color: string;
|
||||||
@ -49,7 +49,7 @@ interface FilterTab {
|
|||||||
* Purpose: Provide filtering options for patient list
|
* Purpose: Provide filtering options for patient list
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Multiple filter options (All, Active, Critical, Discharged)
|
* - Multiple filter options (All, Processed, Pending, Error)
|
||||||
* - Patient count display for each filter
|
* - Patient count display for each filter
|
||||||
* - Visual indicators with icons and colors
|
* - Visual indicators with icons and colors
|
||||||
* - Horizontal scrollable layout
|
* - Horizontal scrollable layout
|
||||||
@ -74,26 +74,26 @@ const FilterTabs: React.FC<FilterTabsProps> = ({
|
|||||||
activeColor: theme.colors.primary,
|
activeColor: theme.colors.primary,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Critical',
|
id: 'processed',
|
||||||
label: 'Critical',
|
label: 'Processed',
|
||||||
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',
|
|
||||||
icon: 'check-circle',
|
icon: 'check-circle',
|
||||||
color: theme.colors.success,
|
color: theme.colors.success,
|
||||||
activeColor: 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) {
|
switch (filterId) {
|
||||||
case 'all':
|
case 'all':
|
||||||
return patientCounts.all;
|
return patientCounts.all;
|
||||||
case 'Critical':
|
case 'processed':
|
||||||
return patientCounts.Critical;
|
return patientCounts.processed;
|
||||||
case 'Emergency':
|
case 'pending':
|
||||||
return patientCounts.Emergency;
|
return patientCounts.pending;
|
||||||
case 'Routine':
|
case 'error':
|
||||||
return patientCounts.Routine;
|
return patientCounts.error;
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -190,9 +190,9 @@ const FilterTabs: React.FC<FilterTabsProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Critical Indicator */}
|
{/* Error Indicator */}
|
||||||
{tab.id === 'Critical' && patientCount > 0 && (
|
{tab.id === 'error' && patientCount > 0 && (
|
||||||
<View style={styles.criticalIndicator}>
|
<View style={styles.errorIndicator}>
|
||||||
<View style={styles.pulseDot} />
|
<View style={styles.pulseDot} />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@ -300,8 +300,8 @@ const styles = StyleSheet.create({
|
|||||||
color: theme.colors.background,
|
color: theme.colors.background,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Critical Indicator
|
// Error Indicator
|
||||||
criticalIndicator: {
|
errorIndicator: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 8,
|
top: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
|
|||||||
@ -14,14 +14,14 @@ import {
|
|||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { theme } from '../../../theme/theme';
|
import { theme } from '../../../theme/theme';
|
||||||
import Icon from 'react-native-vector-icons/Feather';
|
import Icon from 'react-native-vector-icons/Feather';
|
||||||
import { MedicalCase, PatientDetails, Series } from '../../../shared/types';
|
import { PatientData } from '../redux/patientCareSlice';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// INTERFACES
|
// INTERFACES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
interface PatientCardProps {
|
interface PatientCardProps {
|
||||||
patient: MedicalCase;
|
patient: PatientData;
|
||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
onEmergencyPress?: () => void;
|
onEmergencyPress?: () => void;
|
||||||
}
|
}
|
||||||
@ -38,9 +38,9 @@ interface PatientCardProps {
|
|||||||
* Features:
|
* Features:
|
||||||
* - Patient basic information from DICOM data
|
* - Patient basic information from DICOM data
|
||||||
* - Modality and institution information
|
* - Modality and institution information
|
||||||
* - Case type with color coding
|
* - Processing status with color coding
|
||||||
* - Series information
|
* - Series information
|
||||||
* - Time since created
|
* - Time since processed
|
||||||
* - Emergency alert for critical cases
|
* - Emergency alert for critical cases
|
||||||
* - Modern ER-focused design
|
* - 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
|
* @param status - Processing status
|
||||||
* @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
|
|
||||||
* @returns Color configuration object
|
* @returns Color configuration object
|
||||||
*/
|
*/
|
||||||
const getCaseTypeConfig = (type: string) => {
|
const getStatusConfig = (status: string) => {
|
||||||
switch (type) {
|
switch (status.toLowerCase()) {
|
||||||
case 'Critical':
|
case 'processed':
|
||||||
return {
|
|
||||||
color: theme.colors.error,
|
|
||||||
icon: 'alert-triangle',
|
|
||||||
bgColor: '#FFF5F5'
|
|
||||||
};
|
|
||||||
case 'Emergency':
|
|
||||||
return {
|
|
||||||
color: '#FF8C00',
|
|
||||||
icon: 'alert-circle',
|
|
||||||
bgColor: '#FFF8E1'
|
|
||||||
};
|
|
||||||
case 'Routine':
|
|
||||||
return {
|
return {
|
||||||
color: theme.colors.success,
|
color: theme.colors.success,
|
||||||
icon: 'check-circle',
|
icon: 'check-circle',
|
||||||
bgColor: '#F0FFF4'
|
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:
|
default:
|
||||||
return {
|
return {
|
||||||
color: theme.colors.primary,
|
color: theme.colors.primary,
|
||||||
@ -122,13 +99,15 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
|||||||
* @returns Color code
|
* @returns Color code
|
||||||
*/
|
*/
|
||||||
const getModalityColor = (modality: string) => {
|
const getModalityColor = (modality: string) => {
|
||||||
switch (modality) {
|
switch (modality.toUpperCase()) {
|
||||||
case 'CT':
|
case 'CT':
|
||||||
return '#4A90E2';
|
return '#4A90E2';
|
||||||
case 'MR':
|
case 'MR':
|
||||||
return '#7B68EE';
|
return '#7B68EE';
|
||||||
case 'DX':
|
case 'DX':
|
||||||
return '#50C878';
|
return '#50C878';
|
||||||
|
case 'DICOM':
|
||||||
|
return '#FF6B6B';
|
||||||
default:
|
default:
|
||||||
return theme.colors.textSecondary;
|
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
|
// DATA EXTRACTION
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
const patientInfo = patient.patient_info;
|
||||||
const patientData = patientDetails.patientdetails || patientDetails;
|
const seriesCount = patient.series_summary.length;
|
||||||
const series = parseJsonSafely(patientDetails.series);
|
const statusConfig = getStatusConfig(patientInfo.status);
|
||||||
const typeConfig = getCaseTypeConfig(patient.type);
|
const isCritical = patientInfo.report_status === 'Critical' || patientInfo.status === 'Error';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// RENDER HELPERS
|
// RENDER HELPERS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render Case Type Badge
|
* Render Status Badge
|
||||||
*
|
*
|
||||||
* Purpose: Render case type indicator badge
|
* Purpose: Render processing status indicator badge
|
||||||
*/
|
*/
|
||||||
const renderTypeBadge = () => (
|
const renderStatusBadge = () => (
|
||||||
<View style={[styles.typeBadge, { backgroundColor: typeConfig.color }]}>
|
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bgColor, borderColor: statusConfig.color }]}>
|
||||||
<Icon name={typeConfig.icon} size={12} color={theme.colors.background} />
|
<Icon name={statusConfig.icon} size={12} color={statusConfig.color} />
|
||||||
<Text style={styles.typeText}>{patient.type}</Text>
|
<Text style={[styles.statusText, { color: statusConfig.color }]}>{patientInfo.status}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -183,7 +181,7 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
|||||||
* Purpose: Render emergency alert button for critical cases
|
* Purpose: Render emergency alert button for critical cases
|
||||||
*/
|
*/
|
||||||
const renderEmergencyButton = () => {
|
const renderEmergencyButton = () => {
|
||||||
if (patient.type !== 'Critical') {
|
if (!isCritical) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,8 +205,8 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.container,
|
styles.container,
|
||||||
// patient.type === 'Critical' && styles.containerCritical,
|
isCritical && styles.containerCritical,
|
||||||
{ borderLeftColor: typeConfig.color }
|
{ borderLeftColor: statusConfig.color }
|
||||||
]}
|
]}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
activeOpacity={0.7}
|
activeOpacity={0.7}
|
||||||
@ -217,14 +215,14 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
|||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<View style={styles.headerLeft}>
|
<View style={styles.headerLeft}>
|
||||||
<Text style={styles.patientName}>
|
<Text style={styles.patientName}>
|
||||||
{patientData.Name || 'Unknown Patient'}
|
{patientInfo.name || 'Unknown Patient'}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.patientInfo}>
|
<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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.headerRight}>
|
<View style={styles.headerRight}>
|
||||||
{renderTypeBadge()}
|
{renderStatusBadge()}
|
||||||
{renderEmergencyButton()}
|
{renderEmergencyButton()}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -237,27 +235,27 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
|||||||
<Text style={[
|
<Text style={[
|
||||||
styles.infoValue,
|
styles.infoValue,
|
||||||
styles.modalityText,
|
styles.modalityText,
|
||||||
{ color: getModalityColor(patientData.Modality) }
|
{ color: getModalityColor(patientInfo.modality) }
|
||||||
]}>
|
]}>
|
||||||
{patientData.Modality || 'N/A'}
|
{patientInfo.modality || 'N/A'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.infoItem}>
|
<View style={styles.infoItem}>
|
||||||
<Text style={styles.infoLabel}>Status</Text>
|
<Text style={styles.infoLabel}>Files</Text>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.infoValue,
|
styles.infoValue,
|
||||||
{ color: patientData.Status === 'Active' ? theme.colors.success : theme.colors.textSecondary }
|
{ color: theme.colors.primary }
|
||||||
]}>
|
]}>
|
||||||
{patientData.Status || 'Unknown'}
|
{patient.total_files_processed}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.infoItem}>
|
<View style={styles.infoItem}>
|
||||||
<Text style={styles.infoLabel}>Report</Text>
|
<Text style={styles.infoLabel}>Report</Text>
|
||||||
<Text style={[
|
<Text style={[
|
||||||
styles.infoValue,
|
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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -266,7 +264,7 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
|||||||
<View style={styles.institutionRow}>
|
<View style={styles.institutionRow}>
|
||||||
<Icon name="home" size={14} color={theme.colors.textSecondary} />
|
<Icon name="home" size={14} color={theme.colors.textSecondary} />
|
||||||
<Text style={styles.institutionText}>
|
<Text style={styles.institutionText}>
|
||||||
{patientData.InstName || 'Unknown Institution'}
|
{patientInfo.institution || 'Unknown Institution'}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -278,17 +276,22 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
|||||||
<Text style={styles.seriesLabel}>Series Information</Text>
|
<Text style={styles.seriesLabel}>Series Information</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.seriesText}>
|
<Text style={styles.seriesText}>
|
||||||
{Array.isArray(series) ? series.length : 0} Series Available
|
{seriesCount} Series Available • {patientInfo.frame_count} Total Frames
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
|
<View style={styles.footerLeft}>
|
||||||
<Text style={styles.dateText}>
|
<Text style={styles.dateText}>
|
||||||
{formatDate(patient.created_at)}
|
{formatDate(patientInfo.date)}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text style={styles.processedText}>
|
||||||
|
{getTimeSinceProcessed(patient.last_processed_at)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
<View style={styles.footerRight}>
|
<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} />
|
<Icon name="chevron-right" size={16} color={theme.colors.textMuted} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -350,19 +353,19 @@ const styles = StyleSheet.create({
|
|||||||
fontFamily: theme.typography.fontFamily.regular,
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Type Badge
|
// Status Badge
|
||||||
typeBadge: {
|
statusBadge: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
marginRight: theme.spacing.xs,
|
marginRight: theme.spacing.xs,
|
||||||
|
borderWidth: 1,
|
||||||
},
|
},
|
||||||
typeText: {
|
statusText: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: theme.colors.background,
|
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
@ -459,11 +462,20 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
|
footerLeft: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
dateText: {
|
dateText: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: theme.colors.textMuted,
|
color: theme.colors.textMuted,
|
||||||
fontFamily: theme.typography.fontFamily.regular,
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
},
|
},
|
||||||
|
processedText: {
|
||||||
|
fontSize: 11,
|
||||||
|
color: theme.colors.textSecondary,
|
||||||
|
marginTop: 2,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
|
},
|
||||||
footerRight: {
|
footerRight: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@ -49,13 +49,11 @@ export interface PatientsScreenParams {
|
|||||||
*
|
*
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* - patientId: Required patient ID to display details
|
* - patientId: Required patient ID to display details
|
||||||
* - patientName: Required patient name for display
|
* - patientName: Optional patient name for display (will be fetched from API if not provided)
|
||||||
* - medicalCase: Required medical case data with full patient information
|
|
||||||
*/
|
*/
|
||||||
export interface PatientDetailsScreenParams {
|
export interface PatientDetailsScreenParams {
|
||||||
patientId: string;
|
patientId: string;
|
||||||
patientName: string;
|
patientName?: string;
|
||||||
medicalCase: MedicalCase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import { RootState } from '../../../store/store';
|
import { RootState } from '../../../store';
|
||||||
import { MedicalCase } from '../../../shared/types';
|
import { PatientData } from './patientCareSlice';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// BASE SELECTORS
|
// BASE SELECTORS
|
||||||
@ -120,75 +120,59 @@ export const selectLastUpdated = (state: RootState) => state.patientCare.lastUpd
|
|||||||
export const selectFilteredPatients = createSelector(
|
export const selectFilteredPatients = createSelector(
|
||||||
[selectPatients, selectSearchQuery, selectSelectedFilter, selectSortBy, selectSortOrder],
|
[selectPatients, selectSearchQuery, selectSelectedFilter, selectSortBy, selectSortOrder],
|
||||||
(patients, searchQuery, selectedFilter, sortBy, sortOrder) => {
|
(patients, searchQuery, selectedFilter, sortBy, sortOrder) => {
|
||||||
|
// Ensure patients is always an array
|
||||||
|
if (!patients || !Array.isArray(patients)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
let filteredPatients = [...patients];
|
let filteredPatients = [...patients];
|
||||||
|
|
||||||
// Helper function to parse JSON strings safely
|
// Apply filter based on processing status
|
||||||
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
|
|
||||||
if (selectedFilter !== 'all') {
|
if (selectedFilter !== 'all') {
|
||||||
filteredPatients = filteredPatients.filter(
|
filteredPatients = filteredPatients.filter((patient: PatientData) => {
|
||||||
patient => patient.type === selectedFilter
|
const status = patient.patient_info.status.toLowerCase();
|
||||||
);
|
return status === selectedFilter;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply search
|
// Apply search
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
const query = searchQuery.toLowerCase().trim();
|
const query = searchQuery.toLowerCase().trim();
|
||||||
filteredPatients = filteredPatients.filter(patient => {
|
filteredPatients = filteredPatients.filter((patient: PatientData) => {
|
||||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
const patientInfo = patient.patient_info;
|
||||||
const patientData = patientDetails.patientdetails || patientDetails;
|
|
||||||
|
|
||||||
const name = (patientData.Name || '').toLowerCase();
|
const name = (patientInfo.name || '').toLowerCase();
|
||||||
const patId = (patientData.PatID || '').toLowerCase();
|
const patId = (patient.patid || '').toLowerCase();
|
||||||
const instName = (patientData.InstName || '').toLowerCase();
|
const institution = (patientInfo.institution || '').toLowerCase();
|
||||||
const modality = (patientData.Modality || '').toLowerCase();
|
const modality = (patientInfo.modality || '').toLowerCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
name.includes(query) ||
|
name.includes(query) ||
|
||||||
patId.includes(query) ||
|
patId.includes(query) ||
|
||||||
instName.includes(query) ||
|
institution.includes(query) ||
|
||||||
modality.includes(query)
|
modality.includes(query)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting
|
||||||
filteredPatients.sort((a, b) => {
|
filteredPatients.sort((a: PatientData, b: PatientData) => {
|
||||||
const patientDetailsA = parseJsonSafely(a.patientdetails);
|
|
||||||
const patientDataA = patientDetailsA.patientdetails || patientDetailsA;
|
|
||||||
const patientDetailsB = parseJsonSafely(b.patientdetails);
|
|
||||||
const patientDataB = patientDetailsB.patientdetails || patientDetailsB;
|
|
||||||
|
|
||||||
let aValue: any;
|
let aValue: any;
|
||||||
let bValue: any;
|
let bValue: any;
|
||||||
|
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'name':
|
case 'name':
|
||||||
aValue = (patientDataA.Name || '').toLowerCase();
|
aValue = (a.patient_info.name || '').toLowerCase();
|
||||||
bValue = (patientDataB.Name || '').toLowerCase();
|
bValue = (b.patient_info.name || '').toLowerCase();
|
||||||
break;
|
break;
|
||||||
case 'age':
|
case 'processed':
|
||||||
aValue = parseInt(patientDataA.PatAge || '0');
|
aValue = new Date(a.last_processed_at).getTime();
|
||||||
bValue = parseInt(patientDataB.PatAge || '0');
|
bValue = new Date(b.last_processed_at).getTime();
|
||||||
break;
|
break;
|
||||||
case 'date':
|
case 'date':
|
||||||
default:
|
default:
|
||||||
aValue = new Date(a.created_at).getTime();
|
aValue = new Date(a.patient_info.date).getTime();
|
||||||
bValue = new Date(b.created_at).getTime();
|
bValue = new Date(b.patient_info.date).getTime();
|
||||||
break;
|
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],
|
[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],
|
[selectPatients],
|
||||||
(patients: MedicalCase[]) => patients.filter((patient: MedicalCase) => {
|
(patients) => {
|
||||||
// Parse patient details to check status
|
if (!patients || !Array.isArray(patients)) return [];
|
||||||
const parseJsonSafely = (jsonString: string | object) => {
|
return patients.filter((patient: PatientData) =>
|
||||||
if (typeof jsonString === 'object') return jsonString;
|
patient.patient_info.status.toLowerCase() === 'pending'
|
||||||
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';
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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],
|
[selectPatients],
|
||||||
(patients: MedicalCase[]) => {
|
(patients) => {
|
||||||
const grouped: { [key: string]: MedicalCase[] } = {};
|
if (!patients || !Array.isArray(patients)) return [];
|
||||||
|
return patients.filter((patient: PatientData) =>
|
||||||
patients.forEach((patient: MedicalCase) => {
|
patient.patient_info.status.toLowerCase() === 'error'
|
||||||
const dept = patient.type; // Use case type instead of department
|
);
|
||||||
if (!grouped[dept]) {
|
|
||||||
grouped[dept] = [];
|
|
||||||
}
|
}
|
||||||
grouped[dept].push(patient);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select Patients by Modality
|
||||||
|
*
|
||||||
|
* Purpose: Get patients grouped by imaging modality
|
||||||
|
*/
|
||||||
|
export const selectPatientsByModality = createSelector(
|
||||||
|
[selectPatients],
|
||||||
|
(patients) => {
|
||||||
|
if (!patients || !Array.isArray(patients)) return {};
|
||||||
|
|
||||||
|
const grouped: { [key: string]: PatientData[] } = {};
|
||||||
|
|
||||||
|
patients.forEach((patient: PatientData) => {
|
||||||
|
const modality = patient.patient_info.modality || 'Unknown';
|
||||||
|
if (!grouped[modality]) {
|
||||||
|
grouped[modality] = [];
|
||||||
|
}
|
||||||
|
grouped[modality].push(patient);
|
||||||
});
|
});
|
||||||
|
|
||||||
return grouped;
|
return grouped;
|
||||||
@ -266,43 +265,55 @@ export const selectPatientsByDepartment = createSelector(
|
|||||||
*/
|
*/
|
||||||
export const selectPatientStats = createSelector(
|
export const selectPatientStats = createSelector(
|
||||||
[selectPatients],
|
[selectPatients],
|
||||||
(patients: MedicalCase[]) => {
|
(patients) => {
|
||||||
const total = patients.length;
|
if (!patients || !Array.isArray(patients)) {
|
||||||
const critical = patients.filter((p: MedicalCase) => p.type === 'Critical').length;
|
return {
|
||||||
const emergency = patients.filter((p: MedicalCase) => p.type === 'Emergency').length;
|
total: 0,
|
||||||
const routine = patients.filter((p: MedicalCase) => p.type === 'Routine').length;
|
processed: 0,
|
||||||
|
pending: 0,
|
||||||
// Parse patient details for age calculation
|
error: 0,
|
||||||
const parseJsonSafely = (jsonString: string | object) => {
|
averageAge: 0,
|
||||||
if (typeof jsonString === 'object') return jsonString;
|
modalities: {},
|
||||||
if (typeof jsonString === 'string') {
|
totalFiles: 0,
|
||||||
try { return JSON.parse(jsonString); } catch { return {}; }
|
processedPercentage: 0,
|
||||||
}
|
pendingPercentage: 0,
|
||||||
return {};
|
errorPercentage: 0,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const totalAge = patients.reduce((sum: number, patient: MedicalCase) => {
|
const total = patients.length;
|
||||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
const processed = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'processed').length;
|
||||||
const patientData = patientDetails.patientdetails || patientDetails;
|
const pending = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'pending').length;
|
||||||
return sum + parseInt(patientData.PatAge || '0');
|
const error = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'error').length;
|
||||||
|
|
||||||
|
// Calculate average age
|
||||||
|
const totalAge = patients.reduce((sum: number, patient: PatientData) => {
|
||||||
|
const age = parseInt(patient.patient_info.age) || 0;
|
||||||
|
return sum + age;
|
||||||
}, 0);
|
}, 0);
|
||||||
const averageAge = total > 0 ? Math.round(totalAge / total) : 0;
|
const averageAge = total > 0 ? Math.round(totalAge / total) : 0;
|
||||||
|
|
||||||
// Case type distribution
|
// Modality distribution
|
||||||
const caseTypes: { [key: string]: number } = {};
|
const modalities: { [key: string]: number } = {};
|
||||||
patients.forEach((patient: MedicalCase) => {
|
patients.forEach((patient: PatientData) => {
|
||||||
caseTypes[patient.type] = (caseTypes[patient.type] || 0) + 1;
|
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 {
|
return {
|
||||||
total,
|
total,
|
||||||
critical,
|
processed,
|
||||||
emergency,
|
pending,
|
||||||
routine,
|
error,
|
||||||
averageAge,
|
averageAge,
|
||||||
caseTypes,
|
modalities,
|
||||||
criticalPercentage: total > 0 ? Math.round((critical / total) * 100) : 0,
|
totalFiles,
|
||||||
emergencyPercentage: total > 0 ? Math.round((emergency / total) * 100) : 0,
|
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) =>
|
export const selectPatientById = (patientId: string) =>
|
||||||
createSelector(
|
createSelector(
|
||||||
[selectPatients],
|
[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(
|
export const selectPatientsNeedAttention = createSelector(
|
||||||
[selectPatients],
|
[selectPatients],
|
||||||
(patients) => {
|
(patients) => {
|
||||||
return patients.filter(patient => {
|
if (!patients || !Array.isArray(patients)) return [];
|
||||||
// Critical patients always need attention
|
|
||||||
if (patient.priority === 'CRITICAL') return true;
|
|
||||||
|
|
||||||
// Check vital signs for abnormal values
|
return patients.filter((patient: PatientData) => {
|
||||||
const vitals = patient.vitalSigns;
|
// Error patients always need attention
|
||||||
|
if (patient.patient_info.status.toLowerCase() === 'error') return true;
|
||||||
|
|
||||||
// Check blood pressure (hypertensive crisis)
|
// Patients with critical report status
|
||||||
if (vitals.bloodPressure.systolic > 180 || vitals.bloodPressure.diastolic > 120) {
|
if (patient.patient_info.report_status.toLowerCase() === 'critical') return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check heart rate (too high or too low)
|
// Patients with high frame count (complex cases)
|
||||||
if (vitals.heartRate.value > 120 || vitals.heartRate.value < 50) {
|
if (patient.patient_info.frame_count > 100) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check temperature (fever or hypothermia)
|
// Patients with multiple series (complex cases)
|
||||||
if (vitals.temperature.value > 38.5 || vitals.temperature.value < 35) {
|
if (patient.series_summary.length > 5) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check oxygen saturation (low)
|
|
||||||
if (vitals.oxygenSaturation.value < 90) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@ -367,7 +369,7 @@ export const selectPatientsNeedAttention = createSelector(
|
|||||||
*/
|
*/
|
||||||
export const selectHasPatientData = createSelector(
|
export const selectHasPatientData = createSelector(
|
||||||
[selectPatients],
|
[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(
|
export const selectIsEmptyState = createSelector(
|
||||||
[selectPatients, selectPatientsLoading, selectFilteredPatients],
|
[selectPatients, selectPatientsLoading, selectFilteredPatients],
|
||||||
(patients, isLoading, filteredPatients) =>
|
(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,
|
||||||
|
};
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -6,9 +6,77 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { MedicalCase, PatientCareState } from '../../../shared/types';
|
|
||||||
import { patientAPI } from '../services/patientAPI';
|
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
|
// ASYNC THUNKS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -24,84 +92,78 @@ export const fetchPatients = createAsyncThunk(
|
|||||||
'patientCare/fetchPatients',
|
'patientCare/fetchPatients',
|
||||||
async (token: string, { rejectWithValue }) => {
|
async (token: string, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
// Make actual API call to fetch medical cases
|
const response: any = await patientAPI.getPatients(token);
|
||||||
const response :any = await patientAPI.getPatients(token);
|
console.log('response', response);
|
||||||
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) => ({
|
if (response.ok && response.data&& response.data.data) {
|
||||||
...patient,
|
// Return the patients data directly from the new API structure
|
||||||
type: caseTypes[Math.floor(Math.random() * caseTypes.length)]
|
return response.data.data as PatientData[];
|
||||||
}));
|
|
||||||
|
|
||||||
return patientsWithTypes as MedicalCase[];
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to mock data for development
|
// Fallback to mock data for development
|
||||||
const mockPatients: MedicalCase[] = [
|
const mockPatients: PatientData[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
patid: "demo001",
|
||||||
patientdetails: JSON.stringify({
|
hospital_id: "demo-hospital-001",
|
||||||
patientdetails: {
|
first_processed_at: "2025-01-15T10:30:00Z",
|
||||||
Date: '2024-01-15',
|
last_processed_at: "2025-01-15T11:45:00Z",
|
||||||
Name: 'John Doe',
|
total_files_processed: 3,
|
||||||
PatID: 'MRN001',
|
patient_info: {
|
||||||
PatAge: '38',
|
name: "John Doe",
|
||||||
PatSex: 'M',
|
age: "38",
|
||||||
Status: 'Active',
|
sex: "M",
|
||||||
InstName: 'City General Hospital',
|
date: "2025-01-15",
|
||||||
Modality: 'CT',
|
institution: "City General Hospital",
|
||||||
ReportStatus: 'Pending'
|
modality: "CT",
|
||||||
}
|
status: "Processed",
|
||||||
}),
|
report_status: "Available",
|
||||||
series: JSON.stringify([
|
file_name: "chest_ct_001.dcm",
|
||||||
|
file_type: "dcm",
|
||||||
|
frame_count: 50
|
||||||
|
},
|
||||||
|
series_summary: [
|
||||||
{
|
{
|
||||||
Path: ['/dicom/series1'],
|
series_num: "1",
|
||||||
SerDes: 'Chest CT',
|
series_description: "Chest CT",
|
||||||
ViePos: 'Supine',
|
total_images: 50,
|
||||||
pngpath: '/images/ct_chest_1.png',
|
png_preview: "/images/ct_chest_1.png",
|
||||||
SeriesNum: '1',
|
modality: "CT"
|
||||||
ImgTotalinSeries: '50'
|
|
||||||
}
|
}
|
||||||
]),
|
],
|
||||||
created_at: '2024-01-15T10:30:00Z',
|
processing_metadata: {}
|
||||||
updated_at: '2024-01-15T11:45:00Z',
|
|
||||||
series_id: 'series_001',
|
|
||||||
type: 'Critical'
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
patid: "demo002",
|
||||||
patientdetails: JSON.stringify({
|
hospital_id: "demo-hospital-002",
|
||||||
patientdetails: {
|
first_processed_at: "2025-01-15T09:15:00Z",
|
||||||
Date: '2024-01-15',
|
last_processed_at: "2025-01-15T10:30:00Z",
|
||||||
Name: 'Jane Smith',
|
total_files_processed: 2,
|
||||||
PatID: 'MRN002',
|
patient_info: {
|
||||||
PatAge: '33',
|
name: "Jane Smith",
|
||||||
PatSex: 'F',
|
age: "33",
|
||||||
Status: 'Active',
|
sex: "F",
|
||||||
InstName: 'Memorial Medical Center',
|
date: "2025-01-15",
|
||||||
Modality: 'MR',
|
institution: "Memorial Medical Center",
|
||||||
ReportStatus: 'Completed'
|
modality: "MR",
|
||||||
}
|
status: "Processed",
|
||||||
}),
|
report_status: "Available",
|
||||||
series: JSON.stringify([
|
file_name: "brain_mri_001.dcm",
|
||||||
{
|
file_type: "dcm",
|
||||||
Path: ['/dicom/series2'],
|
frame_count: 120
|
||||||
SerDes: 'Brain MRI',
|
|
||||||
ViePos: 'Supine',
|
|
||||||
pngpath: '/images/mri_brain_1.png',
|
|
||||||
SeriesNum: '2',
|
|
||||||
ImgTotalinSeries: '120'
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
created_at: '2024-01-15T09:15:00Z',
|
|
||||||
updated_at: '2024-01-15T10:30:00Z',
|
|
||||||
series_id: 'series_002',
|
|
||||||
type: 'Routine'
|
|
||||||
},
|
},
|
||||||
|
series_summary: [
|
||||||
|
{
|
||||||
|
series_num: "1",
|
||||||
|
series_description: "Brain MRI",
|
||||||
|
total_images: 120,
|
||||||
|
png_preview: "/images/mri_brain_1.png",
|
||||||
|
modality: "MR"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
processing_metadata: {}
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
return mockPatients;
|
return [];
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Fetch patients error:', error);
|
console.error('Fetch patients error:', error);
|
||||||
@ -126,35 +188,35 @@ export const fetchPatientDetails = createAsyncThunk(
|
|||||||
await new Promise((resolve) => setTimeout(resolve as any, 1000));
|
await new Promise((resolve) => setTimeout(resolve as any, 1000));
|
||||||
|
|
||||||
// Mock patient details for specific patient
|
// Mock patient details for specific patient
|
||||||
const mockPatient: MedicalCase = {
|
const mockPatient: PatientData = {
|
||||||
id: parseInt(patientId),
|
patid: patientId,
|
||||||
patientdetails: JSON.stringify({
|
hospital_id: `demo-hospital-${patientId}`,
|
||||||
patientdetails: {
|
first_processed_at: "2025-01-15T10:30:00Z",
|
||||||
Date: '2024-01-15',
|
last_processed_at: "2025-01-15T11:45:00Z",
|
||||||
Name: 'John Doe',
|
total_files_processed: 3,
|
||||||
PatID: `MRN${patientId.padStart(3, '0')}`,
|
patient_info: {
|
||||||
PatAge: '38',
|
name: `Patient ${patientId}`,
|
||||||
PatSex: 'M',
|
age: "38",
|
||||||
Status: 'Active',
|
sex: "M",
|
||||||
InstName: 'City General Hospital',
|
date: "2025-01-15",
|
||||||
Modality: 'CT',
|
institution: "City General Hospital",
|
||||||
ReportStatus: 'Pending'
|
modality: "CT",
|
||||||
}
|
status: "Processed",
|
||||||
}),
|
report_status: "Available",
|
||||||
series: JSON.stringify([
|
file_name: `patient_${patientId}.dcm`,
|
||||||
|
file_type: "dcm",
|
||||||
|
frame_count: 50
|
||||||
|
},
|
||||||
|
series_summary: [
|
||||||
{
|
{
|
||||||
Path: [`/dicom/series${patientId}`],
|
series_num: "1",
|
||||||
SerDes: 'Chest CT',
|
series_description: "Chest CT",
|
||||||
ViePos: 'Supine',
|
total_images: 50,
|
||||||
pngpath: `/images/ct_chest_${patientId}.png`,
|
png_preview: `/images/ct_chest_${patientId}.png`,
|
||||||
SeriesNum: patientId,
|
modality: "CT"
|
||||||
ImgTotalinSeries: '50'
|
|
||||||
}
|
}
|
||||||
]),
|
],
|
||||||
created_at: '2024-01-15T10:30:00Z',
|
processing_metadata: {}
|
||||||
updated_at: '2024-01-15T11:45:00Z',
|
|
||||||
series_id: `series_${patientId.padStart(3, '0')}`,
|
|
||||||
type: 'Critical'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return mockPatient;
|
return mockPatient;
|
||||||
@ -174,7 +236,7 @@ export const fetchPatientDetails = createAsyncThunk(
|
|||||||
*/
|
*/
|
||||||
export const updatePatient = createAsyncThunk(
|
export const updatePatient = createAsyncThunk(
|
||||||
'patientCare/updatePatient',
|
'patientCare/updatePatient',
|
||||||
async (patientData: Partial<MedicalCase> & { id: number }, { rejectWithValue }) => {
|
async (patientData: Partial<PatientData> & { patid: string }, { rejectWithValue }) => {
|
||||||
try {
|
try {
|
||||||
// TODO: Replace with actual API call
|
// TODO: Replace with actual API call
|
||||||
await new Promise((resolve) => setTimeout(resolve as any, 800));
|
await new Promise((resolve) => setTimeout(resolve as any, 800));
|
||||||
@ -200,6 +262,7 @@ export const updatePatient = createAsyncThunk(
|
|||||||
* - Loading states for async operations
|
* - Loading states for async operations
|
||||||
* - Error handling and messages
|
* - Error handling and messages
|
||||||
* - Search and filtering
|
* - Search and filtering
|
||||||
|
* - Pagination and caching
|
||||||
*/
|
*/
|
||||||
const initialState: PatientCareState = {
|
const initialState: PatientCareState = {
|
||||||
// Patients data
|
// Patients data
|
||||||
@ -275,7 +338,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Set patient filter
|
* 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.selectedFilter = action.payload;
|
||||||
state.currentPage = 1; // Reset to first page when filtering
|
state.currentPage = 1; // Reset to first page when filtering
|
||||||
},
|
},
|
||||||
@ -285,7 +348,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Set patient sort options
|
* 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.sortBy = action.payload.by;
|
||||||
state.sortOrder = action.payload.order;
|
state.sortOrder = action.payload.order;
|
||||||
},
|
},
|
||||||
@ -314,7 +377,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Set the currently selected patient
|
* Purpose: Set the currently selected patient
|
||||||
*/
|
*/
|
||||||
setCurrentPatient: (state, action: PayloadAction<MedicalCase | null>) => {
|
setCurrentPatient: (state, action: PayloadAction<PatientData | null>) => {
|
||||||
state.currentPatient = action.payload;
|
state.currentPatient = action.payload;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -323,14 +386,14 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Update a patient in the patients list
|
* Purpose: Update a patient in the patients list
|
||||||
*/
|
*/
|
||||||
updatePatientInList: (state, action: PayloadAction<MedicalCase>) => {
|
updatePatientInList: (state, action: PayloadAction<PatientData>) => {
|
||||||
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) {
|
if (index !== -1) {
|
||||||
state.patients[index] = action.payload;
|
state.patients[index] = action.payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update current patient if it's the same patient
|
// 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;
|
state.currentPatient = action.payload;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -340,7 +403,7 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Add a new patient to the list
|
* Purpose: Add a new patient to the list
|
||||||
*/
|
*/
|
||||||
addPatient: (state, action: PayloadAction<MedicalCase>) => {
|
addPatient: (state, action: PayloadAction<PatientData>) => {
|
||||||
state.patients.unshift(action.payload);
|
state.patients.unshift(action.payload);
|
||||||
state.totalItems += 1;
|
state.totalItems += 1;
|
||||||
},
|
},
|
||||||
@ -350,15 +413,15 @@ const patientCareSlice = createSlice({
|
|||||||
*
|
*
|
||||||
* Purpose: Remove a patient from the list
|
* Purpose: Remove a patient from the list
|
||||||
*/
|
*/
|
||||||
removePatient: (state, action: PayloadAction<number>) => {
|
removePatient: (state, action: PayloadAction<string>) => {
|
||||||
const index = state.patients.findIndex(patient => patient.id === action.payload);
|
const index = state.patients.findIndex(patient => patient.patid === action.payload);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
state.patients.splice(index, 1);
|
state.patients.splice(index, 1);
|
||||||
state.totalItems -= 1;
|
state.totalItems -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear current patient if it's the same patient
|
// 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;
|
state.currentPatient = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -415,13 +478,13 @@ const patientCareSlice = createSlice({
|
|||||||
builder
|
builder
|
||||||
.addCase(updatePatient.fulfilled, (state, action) => {
|
.addCase(updatePatient.fulfilled, (state, action) => {
|
||||||
// Update patient in list
|
// 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) {
|
if (index !== -1) {
|
||||||
state.patients[index] = { ...state.patients[index], ...action.payload };
|
state.patients[index] = { ...state.patients[index], ...action.payload };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update current patient if it's the same patient
|
// 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 };
|
state.currentPatient = { ...state.currentPatient, ...action.payload };
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,62 +5,50 @@
|
|||||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
|
||||||
ScrollView,
|
|
||||||
RefreshControl,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
StatusBar,
|
|
||||||
Alert,
|
|
||||||
FlatList,
|
FlatList,
|
||||||
Dimensions,
|
RefreshControl,
|
||||||
|
SafeAreaView,
|
||||||
|
StatusBar,
|
||||||
|
StyleSheet,
|
||||||
|
Alert,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { theme } from '../../../theme/theme';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||||
import Icon from 'react-native-vector-icons/Feather';
|
import { theme } from '../../../theme/theme';
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
// Import patient care functionality
|
// Components
|
||||||
|
import PatientCard from '../components/PatientCard';
|
||||||
|
import SearchBar from '../components/SearchBar';
|
||||||
|
import FilterTabs from '../components/FilterTabs';
|
||||||
|
import LoadingState from '../components/LoadingState';
|
||||||
|
import EmptyState from '../components/EmptyState';
|
||||||
|
|
||||||
|
// Redux
|
||||||
import {
|
import {
|
||||||
fetchPatients,
|
fetchPatients,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
setFilter,
|
setFilter,
|
||||||
setSort,
|
|
||||||
clearError
|
|
||||||
} from '../redux/patientCareSlice';
|
} from '../redux/patientCareSlice';
|
||||||
|
|
||||||
// Import patient care selectors
|
|
||||||
import {
|
import {
|
||||||
selectPatients,
|
selectPatients,
|
||||||
|
selectFilteredPatients,
|
||||||
selectPatientsLoading,
|
selectPatientsLoading,
|
||||||
selectPatientsError,
|
|
||||||
selectIsRefreshing,
|
selectIsRefreshing,
|
||||||
|
selectPatientsError,
|
||||||
selectSearchQuery,
|
selectSearchQuery,
|
||||||
selectSelectedFilter,
|
selectSelectedFilter,
|
||||||
selectSortBy,
|
selectPatientCounts,
|
||||||
selectFilteredPatients,
|
|
||||||
} from '../redux/patientCareSelectors';
|
} from '../redux/patientCareSelectors';
|
||||||
|
|
||||||
// Import auth selectors
|
// Types
|
||||||
|
import { PatientData } from '../redux/patientCareSlice';
|
||||||
import { selectUser } from '../../Auth/redux/authSelectors';
|
import { selectUser } from '../../Auth/redux/authSelectors';
|
||||||
|
|
||||||
// Import 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 types
|
|
||||||
import { MedicalCase, PatientDetails, Series } from '../../../shared/types';
|
|
||||||
import { PatientsScreenProps } from '../navigation/navigationTypes';
|
|
||||||
|
|
||||||
// Get screen dimensions
|
|
||||||
const { width: screenWidth } = Dimensions.get('window');
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// INTERFACES
|
// INTERFACES
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -77,8 +65,8 @@ const { width: screenWidth } = Dimensions.get('window');
|
|||||||
* Features:
|
* Features:
|
||||||
* - Real-time patient data fetching
|
* - Real-time patient data fetching
|
||||||
* - Search functionality with real-time filtering
|
* - Search functionality with real-time filtering
|
||||||
* - Filter tabs (All, Active, Critical, Discharged)
|
* - Filter tabs (All, Processed, Pending, Error)
|
||||||
* - Sort options (Priority, Name, Date)
|
* - Sort options (Date, Name, Processed)
|
||||||
* - Pull-to-refresh functionality
|
* - Pull-to-refresh functionality
|
||||||
* - Patient cards with vital information
|
* - Patient cards with vital information
|
||||||
* - Navigation to patient details
|
* - Navigation to patient details
|
||||||
@ -86,12 +74,13 @@ const { width: screenWidth } = Dimensions.get('window');
|
|||||||
* - Empty state handling
|
* - Empty state handling
|
||||||
* - Modern ER-focused UI design
|
* - Modern ER-focused UI design
|
||||||
*/
|
*/
|
||||||
const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
const PatientsScreen: React.FC = () => {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// STATE MANAGEMENT
|
// STATE MANAGEMENT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
// Redux state
|
// Redux state
|
||||||
const patients = useAppSelector(selectPatients);
|
const patients = useAppSelector(selectPatients);
|
||||||
@ -101,85 +90,56 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
|||||||
const error = useAppSelector(selectPatientsError);
|
const error = useAppSelector(selectPatientsError);
|
||||||
const searchQuery = useAppSelector(selectSearchQuery);
|
const searchQuery = useAppSelector(selectSearchQuery);
|
||||||
const selectedFilter = useAppSelector(selectSelectedFilter);
|
const selectedFilter = useAppSelector(selectSelectedFilter);
|
||||||
const sortBy = useAppSelector(selectSortBy);
|
const patientCounts = useAppSelector(selectPatientCounts);
|
||||||
|
|
||||||
|
// Auth state
|
||||||
const user = useAppSelector(selectUser);
|
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(() => {
|
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) {
|
if (user?.access_token) {
|
||||||
dispatch(fetchPatients(user.access_token));
|
dispatch(fetchPatients(user.access_token));
|
||||||
}
|
}
|
||||||
}, [dispatch, 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
|
* Handle Refresh
|
||||||
*
|
*
|
||||||
* Purpose: Pull-to-refresh functionality
|
* Purpose: Handle pull-to-refresh functionality
|
||||||
*/
|
*/
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
handleFetchPatients();
|
if (user?.access_token) {
|
||||||
}, [handleFetchPatients]);
|
dispatch(fetchPatients(user.access_token));
|
||||||
|
}
|
||||||
|
}, [dispatch, user?.access_token]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Search
|
* Handle Search
|
||||||
*
|
*
|
||||||
* Purpose: Handle search input changes
|
* Purpose: Handle search query changes
|
||||||
*
|
*
|
||||||
* @param query - Search query string
|
* @param query - Search query string
|
||||||
*/
|
*/
|
||||||
@ -190,106 +150,36 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
|||||||
/**
|
/**
|
||||||
* Handle Filter Change
|
* Handle Filter Change
|
||||||
*
|
*
|
||||||
* Purpose: Handle filter tab selection
|
* Purpose: Update the selected filter and refresh the list
|
||||||
*
|
|
||||||
* @param filter - Selected filter option
|
|
||||||
*/
|
*/
|
||||||
const handleFilterChange = useCallback((filter: 'all' | 'Critical' | 'Routine' | 'Emergency') => {
|
const handleFilterChange = useCallback((filter: 'all' | 'processed' | 'pending' | 'error') => {
|
||||||
dispatch(setFilter(filter));
|
dispatch(setFilter(filter));
|
||||||
}, [dispatch]);
|
}, [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
|
* Handle Patient Press
|
||||||
*
|
*
|
||||||
* Purpose: Navigate to patient details screen
|
* Purpose: Navigate to patient details when a patient card is pressed
|
||||||
*
|
|
||||||
* @param patient - Selected patient
|
|
||||||
*/
|
*/
|
||||||
const handlePatientPress = useCallback((patient: MedicalCase) => {
|
const handlePatientPress = useCallback((patient: PatientData) => {
|
||||||
// Helper function to parse JSON strings safely
|
(navigation as any).navigate('PatientDetails', {
|
||||||
const parseJsonSafely = (jsonString: string | object) => {
|
patientId: patient.patid,
|
||||||
if (typeof jsonString === 'object') {
|
patientName: patient.patient_info.name,
|
||||||
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,
|
|
||||||
});
|
});
|
||||||
}, [navigation]);
|
}, [navigation]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle Emergency Alert
|
* Handle Emergency Alert
|
||||||
*
|
*
|
||||||
* Purpose: Handle emergency alert for critical patients
|
* Purpose: Show emergency alert for critical patients
|
||||||
*
|
|
||||||
* @param patient - Patient with emergency
|
|
||||||
*/
|
*/
|
||||||
const handleEmergencyAlert = useCallback((patient: MedicalCase) => {
|
const handleEmergencyAlert = useCallback((patient: PatientData) => {
|
||||||
// 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;
|
|
||||||
|
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Emergency 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: 'Cancel', style: 'cancel' },
|
||||||
text: 'View Details',
|
{ text: 'View Details', onPress: () => handlePatientPress(patient) },
|
||||||
onPress: () => handlePatientPress(patient),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Call Physician',
|
|
||||||
onPress: () => {
|
|
||||||
// TODO: Implement physician calling functionality
|
|
||||||
Alert.alert('Calling', `Calling attending physician...`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Cancel',
|
|
||||||
style: 'cancel',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}, [handlePatientPress]);
|
}, [handlePatientPress]);
|
||||||
@ -299,18 +189,52 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render Patient Item
|
* Render Patient Card
|
||||||
*
|
*
|
||||||
* Purpose: Render individual patient card
|
* Purpose: Render individual patient card component
|
||||||
*
|
|
||||||
* @param item - Patient data with render info
|
|
||||||
*/
|
*/
|
||||||
const renderPatientItem = ({ item }: { item: MedicalCase }) => (
|
const renderPatientCard = useCallback(({ item }: { item: PatientData }) => (
|
||||||
<PatientCard
|
<PatientCard
|
||||||
patient={item}
|
patient={item}
|
||||||
onPress={() => handlePatientPress(item)}
|
onPress={() => handlePatientPress(item)}
|
||||||
onEmergencyPress={() => handleEmergencyAlert(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,148 +242,120 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
|||||||
*
|
*
|
||||||
* Purpose: Render empty state when no patients found
|
* Purpose: Render empty state when no patients found
|
||||||
*/
|
*/
|
||||||
const renderEmptyState = () => {
|
const renderEmptyState = () => (
|
||||||
if (isLoading) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EmptyState
|
<EmptyState
|
||||||
title={searchQuery ? 'No patients found' : 'No patients available'}
|
title="No Patients Found"
|
||||||
subtitle={
|
subtitle={searchQuery.trim() ?
|
||||||
searchQuery
|
`No patients match "${searchQuery}"` :
|
||||||
? `No patients match "${searchQuery}"`
|
"No patients available at the moment"
|
||||||
: 'Patients will appear here when available'
|
|
||||||
}
|
}
|
||||||
iconName="users"
|
iconName="users"
|
||||||
onRetry={searchQuery ? undefined : handleFetchPatients}
|
onRetry={handleRefresh}
|
||||||
|
retryText="Refresh"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MAIN RENDER
|
// 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 (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
||||||
|
|
||||||
{/* Fixed Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
{renderHeader()}
|
||||||
<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>
|
|
||||||
|
|
||||||
<View style={styles.headerRight}>
|
{/* Search and Filters */}
|
||||||
<TouchableOpacity
|
<View style={styles.searchAndFilters}>
|
||||||
style={styles.headerButton}
|
|
||||||
onPress={handleRefresh}
|
|
||||||
disabled={isRefreshing}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name="refresh-cw"
|
|
||||||
size={20}
|
|
||||||
color={isRefreshing ? theme.colors.textMuted : 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>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Fixed Search and Filter Section */}
|
|
||||||
<View style={styles.fixedSection}>
|
|
||||||
{/* Search Bar */}
|
|
||||||
<View style={styles.searchContainer}>
|
|
||||||
<SearchBar
|
<SearchBar
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChangeText={handleSearch}
|
onChangeText={handleSearch}
|
||||||
placeholder="Search patients by name, MRN, or room..."
|
placeholder="Search patients, ID, institution..."
|
||||||
showFilter
|
|
||||||
onFilterPress={() => setShowSortModal(true)}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
|
||||||
<View style={styles.filterContainer}>
|
|
||||||
<FilterTabs
|
<FilterTabs
|
||||||
selectedFilter={selectedFilter}
|
selectedFilter={selectedFilter}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
patientCounts={{
|
patientCounts={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>
|
</View>
|
||||||
|
|
||||||
{/* Results Summary */}
|
{/* Loading State */}
|
||||||
<View style={styles.resultsSummary}>
|
{isLoading && patients.length === 0 && (
|
||||||
<View style={styles.resultsLeft}>
|
<View style={styles.centerContainer}>
|
||||||
<Icon name="users" size={16} color={theme.colors.textSecondary} />
|
<LoadingState />
|
||||||
<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>
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Scrollable Patient List Only */}
|
{/* 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
|
<FlatList
|
||||||
data={filteredPatients}
|
data={filteredPatients}
|
||||||
renderItem={renderPatientItem}
|
renderItem={renderPatientCard}
|
||||||
keyExtractor={(item,index) => index.toString()}
|
keyExtractor={(item) => item.patid}
|
||||||
ListEmptyComponent={renderEmptyState}
|
contentContainerStyle={styles.listContainer}
|
||||||
contentContainerStyle={[
|
|
||||||
styles.listContent,
|
|
||||||
filteredPatients.length === 0 && styles.emptyListContent
|
|
||||||
]}
|
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
refreshControl={
|
refreshControl={
|
||||||
<RefreshControl
|
<RefreshControl
|
||||||
@ -469,17 +365,20 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
|||||||
tintColor={theme.colors.primary}
|
tintColor={theme.colors.primary}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
// Performance optimizations
|
ListFooterComponent={
|
||||||
// removeClippedSubviews={true}
|
<View style={styles.listFooter}>
|
||||||
// maxToRenderPerBatch={10}
|
<Text style={styles.footerText}>
|
||||||
// windowSize={10}
|
Showing {filteredPatients.length} of {patients.length} patients
|
||||||
// initialNumToRender={8}
|
</Text>
|
||||||
// getItemLayout={(data, index) => ({
|
</View>
|
||||||
// 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>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -489,6 +388,7 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
// Container Styles
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
@ -500,19 +400,17 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: theme.spacing.md,
|
paddingHorizontal: theme.spacing.md,
|
||||||
paddingVertical: theme.spacing.sm,
|
paddingVertical: theme.spacing.md,
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
headerLeft: {
|
headerLeft: {
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
backButton: {
|
headerRight: {
|
||||||
marginRight: theme.spacing.sm,
|
flexDirection: 'row',
|
||||||
padding: theme.spacing.xs,
|
gap: theme.spacing.sm,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
@ -523,89 +421,89 @@ const styles = StyleSheet.create({
|
|||||||
headerSubtitle: {
|
headerSubtitle: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: theme.colors.textSecondary,
|
color: theme.colors.textSecondary,
|
||||||
fontFamily: theme.typography.fontFamily.bold,
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
},
|
},
|
||||||
headerRight: {
|
actionButton: {
|
||||||
flexDirection: 'row',
|
backgroundColor: theme.colors.backgroundAlt,
|
||||||
alignItems: 'center',
|
paddingHorizontal: theme.spacing.md,
|
||||||
},
|
paddingVertical: theme.spacing.sm,
|
||||||
headerButton: {
|
|
||||||
padding: theme.spacing.sm,
|
|
||||||
marginLeft: theme.spacing.xs,
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
notificationBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 6,
|
|
||||||
right: 6,
|
|
||||||
backgroundColor: theme.colors.error,
|
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
width: 16,
|
borderWidth: 1,
|
||||||
height: 16,
|
borderColor: theme.colors.border,
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
},
|
||||||
badgeText: {
|
actionButtonText: {
|
||||||
color: theme.colors.background,
|
color: theme.colors.textSecondary,
|
||||||
fontSize: 10,
|
fontSize: 14,
|
||||||
fontWeight: 'bold',
|
fontWeight: '600',
|
||||||
|
fontFamily: theme.typography.fontFamily.medium,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Fixed Section Styles
|
// Search and Filters
|
||||||
fixedSection: {
|
searchAndFilters: {
|
||||||
paddingHorizontal: theme.spacing.md,
|
paddingHorizontal: theme.spacing.md,
|
||||||
paddingTop: theme.spacing.sm,
|
paddingBottom: theme.spacing.sm,
|
||||||
paddingBottom: theme.spacing.md,
|
|
||||||
backgroundColor: theme.colors.background,
|
backgroundColor: theme.colors.background,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
searchContainer: {
|
|
||||||
marginBottom: theme.spacing.md,
|
// Center Container for States
|
||||||
},
|
centerContainer: {
|
||||||
filterContainer: {
|
flex: 1,
|
||||||
marginBottom: theme.spacing.sm,
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: theme.spacing.md,
|
||||||
},
|
},
|
||||||
|
|
||||||
// List Styles
|
// List Styles
|
||||||
listContent: {
|
listContainer: {
|
||||||
paddingTop: theme.spacing.sm,
|
paddingBottom: theme.spacing.lg,
|
||||||
paddingBottom: theme.spacing.xl,
|
|
||||||
},
|
},
|
||||||
emptyListContent: {
|
listFooter: {
|
||||||
flexGrow: 1,
|
paddingVertical: theme.spacing.md,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
footerText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: theme.colors.textMuted,
|
||||||
|
fontFamily: theme.typography.fontFamily.regular,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Results Summary
|
// Error State Styles
|
||||||
resultsSummary: {
|
errorContainer: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: theme.spacing.sm,
|
padding: theme.spacing.xl,
|
||||||
paddingHorizontal: theme.spacing.sm,
|
|
||||||
backgroundColor: theme.colors.backgroundAlt,
|
|
||||||
borderRadius: 8,
|
|
||||||
marginTop: theme.spacing.xs,
|
|
||||||
},
|
},
|
||||||
resultsLeft: {
|
errorTitle: {
|
||||||
flexDirection: 'row',
|
fontSize: 20,
|
||||||
alignItems: 'center',
|
fontWeight: 'bold',
|
||||||
|
color: theme.colors.error,
|
||||||
|
marginBottom: theme.spacing.sm,
|
||||||
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
resultsText: {
|
errorMessage: {
|
||||||
fontSize: 14,
|
fontSize: 16,
|
||||||
color: theme.colors.textPrimary,
|
|
||||||
fontWeight: '500',
|
|
||||||
marginLeft: theme.spacing.xs,
|
|
||||||
},
|
|
||||||
sortInfo: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
sortText: {
|
|
||||||
fontSize: 12,
|
|
||||||
color: theme.colors.textSecondary,
|
color: theme.colors.textSecondary,
|
||||||
textTransform: 'capitalize',
|
marginBottom: theme.spacing.lg,
|
||||||
marginLeft: theme.spacing.xs,
|
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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -34,11 +34,24 @@ export const patientAPI = {
|
|||||||
* @returns Promise with medical cases data
|
* @returns Promise with medical cases data
|
||||||
*/
|
*/
|
||||||
getPatients: (token: string) => {
|
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
|
* Purpose: Fetch detailed information for a specific patient
|
||||||
*
|
*
|
||||||
@ -47,7 +60,7 @@ export const patientAPI = {
|
|||||||
* @returns Promise with patient details
|
* @returns Promise with patient details
|
||||||
*/
|
*/
|
||||||
getPatientDetails: (patientId: string, token: string) => {
|
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
|
* @returns Promise with updated vital signs
|
||||||
*/
|
*/
|
||||||
updatePatientVitals: (patientId: string, vitalSigns: any, token: string) => {
|
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
|
* Get Patient Medical History
|
||||||
*
|
*
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
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';
|
import { theme } from '../../../theme/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,9 +17,13 @@ import { theme } from '../../../theme/theme';
|
|||||||
*
|
*
|
||||||
* Props:
|
* Props:
|
||||||
* - title: Title text to display in the header
|
* - 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 {
|
interface SettingsHeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
|
showBackButton?: boolean;
|
||||||
|
onBackPress?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,9 +36,22 @@ interface SettingsHeaderProps {
|
|||||||
* - Consistent with app theme
|
* - Consistent with app theme
|
||||||
* - Proper spacing and typography
|
* - Proper spacing and typography
|
||||||
*/
|
*/
|
||||||
export const SettingsHeader: React.FC<SettingsHeaderProps> = ({ title }) => {
|
export const SettingsHeader: React.FC<SettingsHeaderProps> = ({
|
||||||
|
title,
|
||||||
|
showBackButton = false,
|
||||||
|
onBackPress
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<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>
|
<Text style={styles.title}>{title}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
@ -51,6 +69,14 @@ const styles = StyleSheet.create({
|
|||||||
paddingVertical: theme.spacing.lg,
|
paddingVertical: theme.spacing.lg,
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Back button styling
|
||||||
|
backButton: {
|
||||||
|
marginRight: theme.spacing.md,
|
||||||
|
padding: theme.spacing.xs,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Title text styling
|
// Title text styling
|
||||||
@ -58,6 +84,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: theme.typography.fontSize.displayMedium,
|
fontSize: theme.typography.fontSize.displayMedium,
|
||||||
fontFamily: theme.typography.fontFamily.bold,
|
fontFamily: theme.typography.fontFamily.bold,
|
||||||
color: theme.colors.textPrimary,
|
color: theme.colors.textPrimary,
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { SettingsScreen } from '../screens/SettingsScreen';
|
|||||||
// Import navigation types
|
// Import navigation types
|
||||||
import { SettingsStackParamList } from './navigationTypes';
|
import { SettingsStackParamList } from './navigationTypes';
|
||||||
import { theme } from '../../../theme';
|
import { theme } from '../../../theme';
|
||||||
|
import { AppInfoScreen, ChangePasswordScreen, EditProfileScreen } from '../screens';
|
||||||
|
|
||||||
// Create stack navigator for Settings module
|
// Create stack navigator for Settings module
|
||||||
const Stack = createStackNavigator<SettingsStackParamList>();
|
const Stack = createStackNavigator<SettingsStackParamList>();
|
||||||
@ -78,6 +79,38 @@ const SettingsStackNavigator: React.FC = () => {
|
|||||||
headerShown: false, // Hide header for main settings screen
|
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>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,28 +19,21 @@ export type SettingsStackParamList = {
|
|||||||
SettingScreen: SettingsScreenParams;
|
SettingScreen: SettingsScreenParams;
|
||||||
|
|
||||||
// Profile Edit screen - Edit user profile information
|
// Profile Edit screen - Edit user profile information
|
||||||
ProfileEdit: ProfileEditScreenParams;
|
// ProfileEdit: ProfileEditScreenParams;
|
||||||
|
|
||||||
// Security Settings screen - Security and privacy settings
|
// 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 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
|
// 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
|
* 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
|
* HelpSupportScreenProps - Props for HelpSupportScreen component
|
||||||
|
|||||||
486
app/modules/Settings/screens/AppInfoScreen.tsx
Normal file
486
app/modules/Settings/screens/AppInfoScreen.tsx
Normal 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.
|
||||||
|
*/
|
||||||
826
app/modules/Settings/screens/ChangePasswordScreen.tsx
Normal file
826
app/modules/Settings/screens/ChangePasswordScreen.tsx
Normal 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.
|
||||||
|
*/
|
||||||
637
app/modules/Settings/screens/EditProfileScreen.tsx
Normal file
637
app/modules/Settings/screens/EditProfileScreen.tsx
Normal 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.
|
||||||
|
*/
|
||||||
@ -11,7 +11,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
TouchableOpacity,
|
|
||||||
Alert,
|
Alert,
|
||||||
RefreshControl,
|
RefreshControl,
|
||||||
Image,
|
Image,
|
||||||
@ -35,7 +34,6 @@ import {
|
|||||||
selectUserFirstName,
|
selectUserFirstName,
|
||||||
selectUserLastName,
|
selectUserLastName,
|
||||||
selectUserProfilePhoto,
|
selectUserProfilePhoto,
|
||||||
selectNotificationPreferences,
|
|
||||||
selectDashboardSettings
|
selectDashboardSettings
|
||||||
} from '../../Auth/redux/authSelectors';
|
} from '../../Auth/redux/authSelectors';
|
||||||
|
|
||||||
@ -106,7 +104,6 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
const userFirstName = useAppSelector(selectUserFirstName);
|
const userFirstName = useAppSelector(selectUserFirstName);
|
||||||
const userLastName = useAppSelector(selectUserLastName);
|
const userLastName = useAppSelector(selectUserLastName);
|
||||||
const userProfilePhoto = useAppSelector(selectUserProfilePhoto);
|
const userProfilePhoto = useAppSelector(selectUserProfilePhoto);
|
||||||
const notificationPreferences = useAppSelector(selectNotificationPreferences);
|
|
||||||
const dashboardSettings = useAppSelector(selectDashboardSettings);
|
const dashboardSettings = useAppSelector(selectDashboardSettings);
|
||||||
|
|
||||||
|
|
||||||
@ -142,79 +139,10 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
type: 'NAVIGATION',
|
type: 'NAVIGATION',
|
||||||
onPress: () => handleNavigation('CHANGE_PASSWORD'),
|
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',
|
id: 'ABOUT',
|
||||||
@ -236,14 +164,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
type: 'NAVIGATION',
|
type: 'NAVIGATION',
|
||||||
onPress: () => handleNavigation('HELP'),
|
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(() => {
|
useEffect(() => {
|
||||||
setSettingsSections(generateSettingsSections());
|
setSettingsSections(generateSettingsSections());
|
||||||
}, [user, notificationPreferences, dashboardSettings]);
|
}, [user, dashboardSettings]);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// EVENT HANDLERS
|
// EVENT HANDLERS
|
||||||
@ -310,7 +231,29 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
* @param screen - Screen to navigate to
|
* @param screen - Screen to navigate to
|
||||||
*/
|
*/
|
||||||
const handleNavigation = (screen: string) => {
|
const handleNavigation = (screen: string) => {
|
||||||
// TODO: Implement navigation to specific settings screens
|
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);
|
console.log('Navigate to:', screen);
|
||||||
setModalConfig({
|
setModalConfig({
|
||||||
title: 'Navigation',
|
title: 'Navigation',
|
||||||
@ -321,6 +264,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
icon: 'info',
|
icon: 'info',
|
||||||
});
|
});
|
||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -387,14 +331,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
setModalVisible(true);
|
setModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* handleProfilePress Function
|
|
||||||
*
|
|
||||||
* Purpose: Handle profile card press navigation
|
|
||||||
*/
|
|
||||||
const handleProfilePress = () => {
|
|
||||||
handleNavigation('PROFILE');
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// MAIN RENDER
|
// MAIN RENDER
|
||||||
@ -422,7 +359,6 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
{/* Profile card section */}
|
{/* Profile card section */}
|
||||||
{user && (
|
{user && (
|
||||||
<View style={styles.profileCard}>
|
<View style={styles.profileCard}>
|
||||||
<TouchableOpacity onPress={handleProfilePress} activeOpacity={0.7}>
|
|
||||||
<View style={styles.profileHeader}>
|
<View style={styles.profileHeader}>
|
||||||
<View style={styles.profileImageContainer}>
|
<View style={styles.profileImageContainer}>
|
||||||
{user.profile_photo_url ? (
|
{user.profile_photo_url ? (
|
||||||
@ -445,14 +381,9 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
|||||||
{user.display_name || `${user.first_name} ${user.last_name}`}
|
{user.display_name || `${user.first_name} ${user.last_name}`}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={styles.profileEmail}>{user.email}</Text>
|
<Text style={styles.profileEmail}>{user.email}</Text>
|
||||||
<Text style={styles.profileRole}>Physician</Text>
|
<Text style={styles.profileRole}>Radiologist</Text>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.editIcon}>
|
|
||||||
<Text style={styles.editText}>Edit</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -589,18 +520,7 @@ const styles = StyleSheet.create({
|
|||||||
color: theme.colors.primary,
|
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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
17
app/modules/Settings/screens/index.ts
Normal file
17
app/modules/Settings/screens/index.ts
Normal 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.
|
||||||
|
*/
|
||||||
@ -99,7 +99,7 @@ export const MainTabNavigator: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* AI Predictions Tab - AI-powered medical predictions */}
|
{/* AI Predictions Tab - AI-powered medical predictions */}
|
||||||
<Tab.Screen
|
{/* <Tab.Screen
|
||||||
name="AIPredictions"
|
name="AIPredictions"
|
||||||
component={AIPredictionStackNavigator}
|
component={AIPredictionStackNavigator}
|
||||||
options={{
|
options={{
|
||||||
@ -110,7 +110,7 @@ export const MainTabNavigator: React.FC = () => {
|
|||||||
),
|
),
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
}}
|
}}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
{/* Reports Tab - Medical documentation */}
|
{/* Reports Tab - Medical documentation */}
|
||||||
{/* <Tab.Screen
|
{/* <Tab.Screen
|
||||||
|
|||||||
@ -96,8 +96,22 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal
|
|||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (webViewRef.current) {
|
if (webViewRef.current) {
|
||||||
try {
|
try {
|
||||||
|
// Send the URL directly as a string message
|
||||||
webViewRef.current.postMessage(dicomUrl);
|
webViewRef.current.postMessage(dicomUrl);
|
||||||
debugLog('DICOM URL sent successfully');
|
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) {
|
} catch (error) {
|
||||||
debugLog(`Failed to send DICOM URL: ${error}`);
|
debugLog(`Failed to send DICOM URL: ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user