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
|
||||
│ │ │ │ └── DepartmentStats.tsx # Department statistics
|
||||
│ │ │ ├── screens/ # Dashboard screens
|
||||
│ │ │ │ └── ERDashboardScreen.tsx # Main ER dashboard
|
||||
│ │ │ │ └── DashboardScreen.tsx # Main ER dashboard
|
||||
│ │ │ ├── hooks/ # Dashboard custom hooks
|
||||
│ │ │ ├── redux/ # Dashboard state management
|
||||
│ │ │ ├── services/ # Dashboard API services
|
||||
@ -223,7 +223,7 @@ NeoScan_Physician/
|
||||
|
||||
### Dashboard Module
|
||||
**Purpose**: Main ER dashboard with patient monitoring and alerts
|
||||
- **ERDashboardScreen**: Main dashboard with patient list and statistics
|
||||
- **DashboardScreen**: Main dashboard with patient list and statistics
|
||||
- **PatientCard**: Individual patient information display
|
||||
- **CriticalAlerts**: High-priority alert notifications
|
||||
- **QuickActions**: Emergency procedure shortcuts
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">NeoScanPhysician</string>
|
||||
<string name="app_name">Radiologist</string>
|
||||
</resources>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -152,7 +152,12 @@ const AIPredictionStackNavigator: React.FC = () => {
|
||||
{/* AI Prediction Details Screen */}
|
||||
<Stack.Screen
|
||||
name="AIPredictionDetails"
|
||||
component={() => <DicomViewer dicomUrl={'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm'} />}
|
||||
component={() => <DicomViewer
|
||||
dicomUrl={'https://ohif-dicom-json-example.s3.amazonaws.com/LIDC-IDRI-0001/01-01-2000-30178/3000566.000000-03192/1-001.dcm'}
|
||||
debugMode={true}
|
||||
onError={(error) => console.log('DICOM Error:', error)}
|
||||
onLoad={() => console.log('DICOM Viewer loaded successfully')}
|
||||
/>}
|
||||
options={({ navigation, route }) => ({
|
||||
title: 'Create Suggestion',
|
||||
headerLeft: () => (
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { logout } from './authSlice';
|
||||
import { logout, updateUserProfile } from './authSlice';
|
||||
import { authAPI } from '../services/authAPI';
|
||||
import { showError, showSuccess } from '../../../shared/utils/toast';
|
||||
|
||||
@ -42,6 +42,78 @@ export const login = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Thunk to update user profile
|
||||
*/
|
||||
export const updateUserProfileAsync = createAsyncThunk(
|
||||
'auth/updateUserProfile',
|
||||
async (profileData: { first_name: string; last_name: string }, { getState, rejectWithValue, dispatch }) => {
|
||||
try {
|
||||
const state = getState() as any;
|
||||
const user = state.auth.user;
|
||||
const token = user?.access_token;
|
||||
|
||||
if (!user?.user_id || !token) {
|
||||
return rejectWithValue('User not authenticated');
|
||||
}
|
||||
|
||||
const response: any = await authAPI.updateUserProfile(user.user_id, profileData, token);
|
||||
|
||||
if (response.ok && response.data) {
|
||||
// Update local state
|
||||
dispatch(updateUserProfile({
|
||||
first_name: profileData.first_name,
|
||||
last_name: profileData.last_name,
|
||||
display_name: `${profileData.first_name} ${profileData.last_name}`
|
||||
}));
|
||||
|
||||
showSuccess('Profile updated successfully');
|
||||
return response.data;
|
||||
} else {
|
||||
const errorMessage = response.data?.message || response.problem || 'Failed to update profile';
|
||||
showError(errorMessage);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || 'Failed to update profile';
|
||||
showError(errorMessage);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Thunk to change password
|
||||
*/
|
||||
export const changePasswordAsync = createAsyncThunk(
|
||||
'auth/changePassword',
|
||||
async (passwordData: { currentPassword: string; newPassword: string }, { getState, rejectWithValue }) => {
|
||||
try {
|
||||
const state = getState() as any;
|
||||
const user = state.auth.user;
|
||||
const token = user?.access_token;
|
||||
|
||||
if (!user?.user_id || !token) {
|
||||
return rejectWithValue('User not authenticated');
|
||||
}
|
||||
|
||||
const response: any = await authAPI.changePassword(user.user_id, { password: passwordData.newPassword }, token);
|
||||
|
||||
if (response.ok && response.data) {
|
||||
showSuccess('Password changed successfully');
|
||||
return response.data;
|
||||
} else {
|
||||
const errorMessage = response.data?.message || response.problem || 'Failed to change password';
|
||||
showError(errorMessage);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || 'Failed to change password';
|
||||
showError(errorMessage);
|
||||
return rejectWithValue(errorMessage);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Thunk to logout user
|
||||
|
||||
@ -138,8 +138,8 @@ const LoginScreen: React.FC<LoginScreenProps> = ({ navigation }) => {
|
||||
* HEADER SECTION - App branding and title
|
||||
* ======================================================================== */}
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Physician</Text>
|
||||
<Text style={styles.subtitle}>Emergency Department Access</Text>
|
||||
<Text style={styles.title}>Radiologist</Text>
|
||||
{/* <Text style={styles.subtitle}>Emergency Department Access</Text> */}
|
||||
</View>
|
||||
<View style={styles.imageContainer}>
|
||||
<Image source={require('../../../assets/images/hospital-logo.png')} style={styles.image} />
|
||||
|
||||
@ -226,7 +226,7 @@ const SignUpScreen: React.FC<SignUpScreenProps> = ({ navigation }) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
let role = 'er_physician';
|
||||
let role = 'radiologist';
|
||||
|
||||
// Prepare form data with proper file handling
|
||||
const formFields = {
|
||||
|
||||
@ -38,7 +38,27 @@ export const authAPI = {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
...(token && { 'Authorization': `Bearer ${token}` }),
|
||||
},
|
||||
})
|
||||
}),
|
||||
|
||||
// Update user profile
|
||||
updateUserProfile: (userId: string, profileData: {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
}, token: string) => api.put(
|
||||
`/api/auth/auth/admin/users/self/${userId}`,
|
||||
profileData,
|
||||
buildHeaders({ token })
|
||||
),
|
||||
|
||||
// Change password (admin endpoint)
|
||||
changePassword: (userId: string, passwordData: {
|
||||
password: string;
|
||||
}, token: string) => api.put(
|
||||
`/api/auth/auth/admin/users/self/${userId}`,
|
||||
passwordData,
|
||||
buildHeaders({ token })
|
||||
),
|
||||
|
||||
// Add more endpoints as needed
|
||||
};
|
||||
|
||||
|
||||
@ -29,33 +29,6 @@ export const DashboardHeader: React.FC<DashboardHeaderProps> = ({
|
||||
{dashboard.shiftInfo.currentShift} Shift • {dashboard.shiftInfo.attendingPhysician}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.statsContainer}>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{dashboard.totalPatients}</Text>
|
||||
<Text style={styles.statLabel}>Total Patients</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={[styles.statValue, styles.criticalValue]}>
|
||||
{dashboard.criticalPatients}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>Critical</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{dashboard.pendingScans}</Text>
|
||||
<Text style={styles.statLabel}>Pending Scans</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Text style={styles.statValue}>{dashboard.bedOccupancy}%</Text>
|
||||
<Text style={styles.statLabel}>Bed Occupancy</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.lastUpdated}>
|
||||
<Text style={styles.lastUpdatedText}>
|
||||
Last updated: {dashboard.lastUpdated.toLocaleTimeString()}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@ -68,9 +41,6 @@ const styles = StyleSheet.create({
|
||||
marginBottom: theme.spacing.lg,
|
||||
...theme.shadows.medium,
|
||||
},
|
||||
header: {
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.typography.fontSize.displayMedium,
|
||||
fontFamily: theme.typography.fontFamily.bold,
|
||||
@ -81,37 +51,7 @@ const styles = StyleSheet.create({
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
color: theme.colors.textSecondary,
|
||||
},
|
||||
statsContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
statItem: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
statValue: {
|
||||
fontSize: theme.typography.fontSize.displaySmall,
|
||||
fontFamily: theme.typography.fontFamily.bold,
|
||||
color: theme.colors.primary,
|
||||
marginBottom: theme.spacing.xs,
|
||||
},
|
||||
criticalValue: {
|
||||
color: theme.colors.critical,
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: theme.typography.fontSize.bodySmall,
|
||||
color: theme.colors.textSecondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
lastUpdated: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
lastUpdatedText: {
|
||||
fontSize: theme.typography.fontSize.caption,
|
||||
color: theme.colors.textMuted,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* End of File: DashboardHeader.tsx
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
// Export screens
|
||||
export { default as ERDashboardScreen } from './screens/ERDashboardScreen';
|
||||
export { default as DashboardScreen } from './screens/DashboardScreen';
|
||||
|
||||
// Export navigation
|
||||
export {
|
||||
@ -14,7 +14,7 @@ export {
|
||||
DashboardStackParamList,
|
||||
DashboardNavigationProp,
|
||||
DashboardScreenProps,
|
||||
ERDashboardScreenProps,
|
||||
DashboardScreenProps,
|
||||
PatientDetailsScreenProps,
|
||||
AlertDetailsScreenProps,
|
||||
DepartmentStatsScreenProps,
|
||||
|
||||
@ -9,7 +9,7 @@ import React from 'react';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
|
||||
// Import dashboard screens
|
||||
import { ERDashboardScreen } from '../screens/ERDashboardScreen';
|
||||
import { DashboardScreen } from '../screens/DashboardScreen';
|
||||
|
||||
// Import navigation types
|
||||
import { DashboardStackParamList } from './navigationTypes';
|
||||
@ -22,7 +22,7 @@ const Stack = createStackNavigator<DashboardStackParamList>();
|
||||
* DashboardStackNavigator - Manages navigation between dashboard screens
|
||||
*
|
||||
* This navigator handles the flow between:
|
||||
* - ERDashboardScreen: Main ER dashboard with patient overview
|
||||
* - DashboardScreen: Main ER dashboard with patient overview
|
||||
* - Future screens: Patient details, alerts, reports, etc.
|
||||
*
|
||||
* Features:
|
||||
@ -72,7 +72,7 @@ const DashboardStackNavigator: React.FC = () => {
|
||||
{/* ER Dashboard Screen - Main dashboard entry point */}
|
||||
<Stack.Screen
|
||||
name="ERDashboard"
|
||||
component={ERDashboardScreen}
|
||||
component={DashboardScreen}
|
||||
options={{
|
||||
title: 'ER Dashboard',
|
||||
headerShown: false, // Hide header for main dashboard
|
||||
|
||||
@ -13,12 +13,12 @@ export type {
|
||||
DashboardStackParamList,
|
||||
DashboardNavigationProp,
|
||||
DashboardScreenProps,
|
||||
ERDashboardScreenProps,
|
||||
DashboardScreenProps,
|
||||
PatientDetailsScreenProps,
|
||||
AlertDetailsScreenProps,
|
||||
DepartmentStatsScreenProps,
|
||||
QuickActionsScreenProps,
|
||||
ERDashboardScreenParams,
|
||||
DashboardScreenParams,
|
||||
PatientDetailsScreenParams,
|
||||
AlertDetailsScreenParams,
|
||||
DepartmentStatsScreenParams,
|
||||
|
||||
@ -16,7 +16,7 @@ import { Patient, Alert as AlertType, ERDashboard } from '../../../shared/types'
|
||||
*/
|
||||
export type DashboardStackParamList = {
|
||||
// ER Dashboard screen - Main dashboard with patient overview
|
||||
ERDashboard: ERDashboardScreenParams;
|
||||
ERDashboard: DashboardScreenParams;
|
||||
|
||||
// Patient Details screen - Detailed patient information
|
||||
PatientDetails: PatientDetailsScreenParams;
|
||||
@ -59,7 +59,7 @@ export interface DashboardScreenProps<T extends keyof DashboardStackParamList> {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* ERDashboardScreenParams
|
||||
* DashboardScreenParams
|
||||
*
|
||||
* Purpose: Parameters passed to the ER dashboard screen
|
||||
*
|
||||
@ -67,7 +67,7 @@ export interface DashboardScreenProps<T extends keyof DashboardStackParamList> {
|
||||
* - filter: Optional filter to apply to dashboard data
|
||||
* - refresh: Optional flag to force refresh
|
||||
*/
|
||||
export interface ERDashboardScreenParams {
|
||||
export interface DashboardScreenParams {
|
||||
filter?: 'all' | 'critical' | 'active' | 'pending';
|
||||
refresh?: boolean;
|
||||
}
|
||||
@ -140,9 +140,9 @@ export interface QuickActionsScreenParams {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* ERDashboardScreenProps - Props for ERDashboardScreen component
|
||||
* DashboardScreenProps - Props for DashboardScreen component
|
||||
*/
|
||||
export type ERDashboardScreenProps = DashboardScreenProps<'ERDashboard'>;
|
||||
export type DashboardScreenProps = DashboardScreenProps<'ERDashboard'>;
|
||||
|
||||
/**
|
||||
* PatientDetailsScreenProps - Props for PatientDetailsScreen component
|
||||
|
||||
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 { CriticalAlerts } from '../components/CriticalAlerts';
|
||||
import { DashboardHeader } from '../components/DashboardHeader';
|
||||
import { QuickActions } from '../components/QuickActions';
|
||||
import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview';
|
||||
|
||||
/**
|
||||
@ -597,13 +596,6 @@ export const ERDashboardScreen: React.FC<ERDashboardScreenProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Quick action buttons for brain imaging tasks */}
|
||||
<QuickActions
|
||||
onQuickAction={(action) => {
|
||||
console.log('Quick action:', action);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Department statistics showing brain case distribution */}
|
||||
{dashboard && <BrainPredictionsOverview dashboard={dashboard} />}
|
||||
|
||||
|
||||
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 {
|
||||
selectedFilter: 'all' | 'Critical' | 'Routine' | 'Emergency';
|
||||
onFilterChange: (filter: 'all' | 'Critical' | 'Routine' | 'Emergency') => void;
|
||||
selectedFilter: 'all' | 'processed' | 'pending' | 'error';
|
||||
onFilterChange: (filter: 'all' | 'processed' | 'pending' | 'error') => void;
|
||||
patientCounts: {
|
||||
all: number;
|
||||
Critical: number;
|
||||
Routine: number;
|
||||
Emergency: number;
|
||||
processed: number;
|
||||
pending: number;
|
||||
error: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface FilterTab {
|
||||
id: 'all' | 'Critical' | 'Routine' | 'Emergency';
|
||||
id: 'all' | 'processed' | 'pending' | 'error';
|
||||
label: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
@ -49,7 +49,7 @@ interface FilterTab {
|
||||
* Purpose: Provide filtering options for patient list
|
||||
*
|
||||
* Features:
|
||||
* - Multiple filter options (All, Active, Critical, Discharged)
|
||||
* - Multiple filter options (All, Processed, Pending, Error)
|
||||
* - Patient count display for each filter
|
||||
* - Visual indicators with icons and colors
|
||||
* - Horizontal scrollable layout
|
||||
@ -74,26 +74,26 @@ const FilterTabs: React.FC<FilterTabsProps> = ({
|
||||
activeColor: theme.colors.primary,
|
||||
},
|
||||
{
|
||||
id: 'Critical',
|
||||
label: 'Critical',
|
||||
icon: 'alert-triangle',
|
||||
color: theme.colors.error,
|
||||
activeColor: theme.colors.error,
|
||||
},
|
||||
{
|
||||
id: 'Emergency',
|
||||
label: 'Emergency',
|
||||
icon: 'alert-circle',
|
||||
color: '#FF8C00',
|
||||
activeColor: '#FF8C00',
|
||||
},
|
||||
{
|
||||
id: 'Routine',
|
||||
label: 'Routine',
|
||||
id: 'processed',
|
||||
label: 'Processed',
|
||||
icon: 'check-circle',
|
||||
color: theme.colors.success,
|
||||
activeColor: theme.colors.success,
|
||||
},
|
||||
{
|
||||
id: 'pending',
|
||||
label: 'Pending',
|
||||
icon: 'clock',
|
||||
color: theme.colors.warning,
|
||||
activeColor: theme.colors.warning,
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
label: 'Error',
|
||||
icon: 'alert-triangle',
|
||||
color: theme.colors.error,
|
||||
activeColor: theme.colors.error,
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
@ -112,12 +112,12 @@ const FilterTabs: React.FC<FilterTabsProps> = ({
|
||||
switch (filterId) {
|
||||
case 'all':
|
||||
return patientCounts.all;
|
||||
case 'Critical':
|
||||
return patientCounts.Critical;
|
||||
case 'Emergency':
|
||||
return patientCounts.Emergency;
|
||||
case 'Routine':
|
||||
return patientCounts.Routine;
|
||||
case 'processed':
|
||||
return patientCounts.processed;
|
||||
case 'pending':
|
||||
return patientCounts.pending;
|
||||
case 'error':
|
||||
return patientCounts.error;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@ -190,9 +190,9 @@ const FilterTabs: React.FC<FilterTabsProps> = ({
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Critical Indicator */}
|
||||
{tab.id === 'Critical' && patientCount > 0 && (
|
||||
<View style={styles.criticalIndicator}>
|
||||
{/* Error Indicator */}
|
||||
{tab.id === 'error' && patientCount > 0 && (
|
||||
<View style={styles.errorIndicator}>
|
||||
<View style={styles.pulseDot} />
|
||||
</View>
|
||||
)}
|
||||
@ -300,8 +300,8 @@ const styles = StyleSheet.create({
|
||||
color: theme.colors.background,
|
||||
},
|
||||
|
||||
// Critical Indicator
|
||||
criticalIndicator: {
|
||||
// Error Indicator
|
||||
errorIndicator: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
|
||||
@ -14,14 +14,14 @@ import {
|
||||
} from 'react-native';
|
||||
import { theme } from '../../../theme/theme';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { MedicalCase, PatientDetails, Series } from '../../../shared/types';
|
||||
import { PatientData } from '../redux/patientCareSlice';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
// ============================================================================
|
||||
|
||||
interface PatientCardProps {
|
||||
patient: MedicalCase;
|
||||
patient: PatientData;
|
||||
onPress: () => void;
|
||||
onEmergencyPress?: () => void;
|
||||
}
|
||||
@ -38,9 +38,9 @@ interface PatientCardProps {
|
||||
* Features:
|
||||
* - Patient basic information from DICOM data
|
||||
* - Modality and institution information
|
||||
* - Case type with color coding
|
||||
* - Processing status with color coding
|
||||
* - Series information
|
||||
* - Time since created
|
||||
* - Time since processed
|
||||
* - Emergency alert for critical cases
|
||||
* - Modern ER-focused design
|
||||
*/
|
||||
@ -54,56 +54,33 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Parse JSON strings safely
|
||||
* Get Status Color Configuration
|
||||
*
|
||||
* Purpose: Handle JSON string or object parsing for patient data
|
||||
* Purpose: Get color and icon based on processing status
|
||||
*
|
||||
* @param jsonString - JSON string or object
|
||||
* @returns Parsed object or empty object
|
||||
*/
|
||||
const parseJsonSafely = (jsonString: string | object) => {
|
||||
if (typeof jsonString === 'object') {
|
||||
return jsonString;
|
||||
}
|
||||
if (typeof jsonString === 'string') {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse JSON:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Case Type Color Configuration
|
||||
*
|
||||
* Purpose: Get color and icon based on case type
|
||||
*
|
||||
* @param type - Case type
|
||||
* @param status - Processing status
|
||||
* @returns Color configuration object
|
||||
*/
|
||||
const getCaseTypeConfig = (type: string) => {
|
||||
switch (type) {
|
||||
case 'Critical':
|
||||
return {
|
||||
color: theme.colors.error,
|
||||
icon: 'alert-triangle',
|
||||
bgColor: '#FFF5F5'
|
||||
};
|
||||
case 'Emergency':
|
||||
return {
|
||||
color: '#FF8C00',
|
||||
icon: 'alert-circle',
|
||||
bgColor: '#FFF8E1'
|
||||
};
|
||||
case 'Routine':
|
||||
const getStatusConfig = (status: string) => {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'processed':
|
||||
return {
|
||||
color: theme.colors.success,
|
||||
icon: 'check-circle',
|
||||
bgColor: '#F0FFF4'
|
||||
};
|
||||
case 'pending':
|
||||
return {
|
||||
color: theme.colors.warning,
|
||||
icon: 'clock',
|
||||
bgColor: '#FFF8E1'
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
color: theme.colors.error,
|
||||
icon: 'alert-triangle',
|
||||
bgColor: '#FFF5F5'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: theme.colors.primary,
|
||||
@ -122,13 +99,15 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
* @returns Color code
|
||||
*/
|
||||
const getModalityColor = (modality: string) => {
|
||||
switch (modality) {
|
||||
switch (modality.toUpperCase()) {
|
||||
case 'CT':
|
||||
return '#4A90E2';
|
||||
case 'MR':
|
||||
return '#7B68EE';
|
||||
case 'DX':
|
||||
return '#50C878';
|
||||
case 'DICOM':
|
||||
return '#FF6B6B';
|
||||
default:
|
||||
return theme.colors.textSecondary;
|
||||
}
|
||||
@ -152,28 +131,47 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get Time Since Processed
|
||||
*
|
||||
* Purpose: Get human-readable time since last processed
|
||||
*
|
||||
* @param dateString - ISO date string
|
||||
* @returns Formatted time string
|
||||
*/
|
||||
const getTimeSinceProcessed = (dateString: string) => {
|
||||
const now = new Date();
|
||||
const processed = new Date(dateString);
|
||||
const diffInMinutes = Math.floor((now.getTime() - processed.getTime()) / (1000 * 60));
|
||||
|
||||
if (diffInMinutes < 1) return 'Just now';
|
||||
if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
|
||||
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`;
|
||||
return `${Math.floor(diffInMinutes / 1440)}d ago`;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// DATA EXTRACTION
|
||||
// ============================================================================
|
||||
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
const series = parseJsonSafely(patientDetails.series);
|
||||
const typeConfig = getCaseTypeConfig(patient.type);
|
||||
const patientInfo = patient.patient_info;
|
||||
const seriesCount = patient.series_summary.length;
|
||||
const statusConfig = getStatusConfig(patientInfo.status);
|
||||
const isCritical = patientInfo.report_status === 'Critical' || patientInfo.status === 'Error';
|
||||
|
||||
// ============================================================================
|
||||
// RENDER HELPERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Render Case Type Badge
|
||||
* Render Status Badge
|
||||
*
|
||||
* Purpose: Render case type indicator badge
|
||||
* Purpose: Render processing status indicator badge
|
||||
*/
|
||||
const renderTypeBadge = () => (
|
||||
<View style={[styles.typeBadge, { backgroundColor: typeConfig.color }]}>
|
||||
<Icon name={typeConfig.icon} size={12} color={theme.colors.background} />
|
||||
<Text style={styles.typeText}>{patient.type}</Text>
|
||||
const renderStatusBadge = () => (
|
||||
<View style={[styles.statusBadge, { backgroundColor: statusConfig.bgColor, borderColor: statusConfig.color }]}>
|
||||
<Icon name={statusConfig.icon} size={12} color={statusConfig.color} />
|
||||
<Text style={[styles.statusText, { color: statusConfig.color }]}>{patientInfo.status}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
@ -183,7 +181,7 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
* Purpose: Render emergency alert button for critical cases
|
||||
*/
|
||||
const renderEmergencyButton = () => {
|
||||
if (patient.type !== 'Critical') {
|
||||
if (!isCritical) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -207,8 +205,8 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.container,
|
||||
// patient.type === 'Critical' && styles.containerCritical,
|
||||
{ borderLeftColor: typeConfig.color }
|
||||
isCritical && styles.containerCritical,
|
||||
{ borderLeftColor: statusConfig.color }
|
||||
]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
@ -217,14 +215,14 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<Text style={styles.patientName}>
|
||||
{patientData.Name || 'Unknown Patient'}
|
||||
{patientInfo.name || 'Unknown Patient'}
|
||||
</Text>
|
||||
<Text style={styles.patientInfo}>
|
||||
ID: {patientData.PatID || 'N/A'} • {patientData.PatAge || 'N/A'}y • {patientData.PatSex || 'N/A'}
|
||||
ID: {patient.patid} • {patientInfo.age || 'N/A'}y • {patientInfo.sex || 'N/A'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.headerRight}>
|
||||
{renderTypeBadge()}
|
||||
{renderStatusBadge()}
|
||||
{renderEmergencyButton()}
|
||||
</View>
|
||||
</View>
|
||||
@ -237,27 +235,27 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
<Text style={[
|
||||
styles.infoValue,
|
||||
styles.modalityText,
|
||||
{ color: getModalityColor(patientData.Modality) }
|
||||
{ color: getModalityColor(patientInfo.modality) }
|
||||
]}>
|
||||
{patientData.Modality || 'N/A'}
|
||||
{patientInfo.modality || 'N/A'}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>Status</Text>
|
||||
<Text style={styles.infoLabel}>Files</Text>
|
||||
<Text style={[
|
||||
styles.infoValue,
|
||||
{ color: patientData.Status === 'Active' ? theme.colors.success : theme.colors.textSecondary }
|
||||
{ color: theme.colors.primary }
|
||||
]}>
|
||||
{patientData.Status || 'Unknown'}
|
||||
{patient.total_files_processed}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.infoItem}>
|
||||
<Text style={styles.infoLabel}>Report</Text>
|
||||
<Text style={[
|
||||
styles.infoValue,
|
||||
{ color: patientData.ReportStatus === 'Completed' ? theme.colors.success : theme.colors.warning }
|
||||
{ color: patientInfo.report_status === 'Available' ? theme.colors.success : theme.colors.warning }
|
||||
]}>
|
||||
{patientData.ReportStatus || 'Pending'}
|
||||
{patientInfo.report_status || 'Pending'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@ -266,7 +264,7 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
<View style={styles.institutionRow}>
|
||||
<Icon name="home" size={14} color={theme.colors.textSecondary} />
|
||||
<Text style={styles.institutionText}>
|
||||
{patientData.InstName || 'Unknown Institution'}
|
||||
{patientInfo.institution || 'Unknown Institution'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
@ -278,17 +276,22 @@ const PatientCard: React.FC<PatientCardProps> = ({
|
||||
<Text style={styles.seriesLabel}>Series Information</Text>
|
||||
</View>
|
||||
<Text style={styles.seriesText}>
|
||||
{Array.isArray(series) ? series.length : 0} Series Available
|
||||
{seriesCount} Series Available • {patientInfo.frame_count} Total Frames
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.dateText}>
|
||||
{formatDate(patient.created_at)}
|
||||
</Text>
|
||||
<View style={styles.footerLeft}>
|
||||
<Text style={styles.dateText}>
|
||||
{formatDate(patientInfo.date)}
|
||||
</Text>
|
||||
<Text style={styles.processedText}>
|
||||
{getTimeSinceProcessed(patient.last_processed_at)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.footerRight}>
|
||||
<Text style={styles.caseId}>Case #{patient.id}</Text>
|
||||
<Text style={styles.caseId}>Case #{patient.patid}</Text>
|
||||
<Icon name="chevron-right" size={16} color={theme.colors.textMuted} />
|
||||
</View>
|
||||
</View>
|
||||
@ -350,19 +353,19 @@ const styles = StyleSheet.create({
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
},
|
||||
|
||||
// Type Badge
|
||||
typeBadge: {
|
||||
// Status Badge
|
||||
statusBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
marginRight: theme.spacing.xs,
|
||||
borderWidth: 1,
|
||||
},
|
||||
typeText: {
|
||||
statusText: {
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
color: theme.colors.background,
|
||||
marginLeft: 4,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
@ -459,11 +462,20 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerLeft: {
|
||||
flex: 1,
|
||||
},
|
||||
dateText: {
|
||||
fontSize: 12,
|
||||
color: theme.colors.textMuted,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
},
|
||||
processedText: {
|
||||
fontSize: 11,
|
||||
color: theme.colors.textSecondary,
|
||||
marginTop: 2,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
},
|
||||
footerRight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
|
||||
@ -49,13 +49,11 @@ export interface PatientsScreenParams {
|
||||
*
|
||||
* Parameters:
|
||||
* - patientId: Required patient ID to display details
|
||||
* - patientName: Required patient name for display
|
||||
* - medicalCase: Required medical case data with full patient information
|
||||
* - patientName: Optional patient name for display (will be fetched from API if not provided)
|
||||
*/
|
||||
export interface PatientDetailsScreenParams {
|
||||
patientId: string;
|
||||
patientName: string;
|
||||
medicalCase: MedicalCase;
|
||||
patientName?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { RootState } from '../../../store/store';
|
||||
import { MedicalCase } from '../../../shared/types';
|
||||
import { RootState } from '../../../store';
|
||||
import { PatientData } from './patientCareSlice';
|
||||
|
||||
// ============================================================================
|
||||
// BASE SELECTORS
|
||||
@ -120,75 +120,59 @@ export const selectLastUpdated = (state: RootState) => state.patientCare.lastUpd
|
||||
export const selectFilteredPatients = createSelector(
|
||||
[selectPatients, selectSearchQuery, selectSelectedFilter, selectSortBy, selectSortOrder],
|
||||
(patients, searchQuery, selectedFilter, sortBy, sortOrder) => {
|
||||
// Ensure patients is always an array
|
||||
if (!patients || !Array.isArray(patients)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let filteredPatients = [...patients];
|
||||
|
||||
// Helper function to parse JSON strings safely
|
||||
const parseJsonSafely = (jsonString: string | object) => {
|
||||
if (typeof jsonString === 'object') {
|
||||
return jsonString;
|
||||
}
|
||||
if (typeof jsonString === 'string') {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse JSON:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
// Apply filter
|
||||
// Apply filter based on processing status
|
||||
if (selectedFilter !== 'all') {
|
||||
filteredPatients = filteredPatients.filter(
|
||||
patient => patient.type === selectedFilter
|
||||
);
|
||||
filteredPatients = filteredPatients.filter((patient: PatientData) => {
|
||||
const status = patient.patient_info.status.toLowerCase();
|
||||
return status === selectedFilter;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply search
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase().trim();
|
||||
filteredPatients = filteredPatients.filter(patient => {
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
filteredPatients = filteredPatients.filter((patient: PatientData) => {
|
||||
const patientInfo = patient.patient_info;
|
||||
|
||||
const name = (patientData.Name || '').toLowerCase();
|
||||
const patId = (patientData.PatID || '').toLowerCase();
|
||||
const instName = (patientData.InstName || '').toLowerCase();
|
||||
const modality = (patientData.Modality || '').toLowerCase();
|
||||
const name = (patientInfo.name || '').toLowerCase();
|
||||
const patId = (patient.patid || '').toLowerCase();
|
||||
const institution = (patientInfo.institution || '').toLowerCase();
|
||||
const modality = (patientInfo.modality || '').toLowerCase();
|
||||
|
||||
return (
|
||||
name.includes(query) ||
|
||||
patId.includes(query) ||
|
||||
instName.includes(query) ||
|
||||
institution.includes(query) ||
|
||||
modality.includes(query)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
filteredPatients.sort((a, b) => {
|
||||
const patientDetailsA = parseJsonSafely(a.patientdetails);
|
||||
const patientDataA = patientDetailsA.patientdetails || patientDetailsA;
|
||||
const patientDetailsB = parseJsonSafely(b.patientdetails);
|
||||
const patientDataB = patientDetailsB.patientdetails || patientDetailsB;
|
||||
|
||||
filteredPatients.sort((a: PatientData, b: PatientData) => {
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
switch (sortBy) {
|
||||
case 'name':
|
||||
aValue = (patientDataA.Name || '').toLowerCase();
|
||||
bValue = (patientDataB.Name || '').toLowerCase();
|
||||
aValue = (a.patient_info.name || '').toLowerCase();
|
||||
bValue = (b.patient_info.name || '').toLowerCase();
|
||||
break;
|
||||
case 'age':
|
||||
aValue = parseInt(patientDataA.PatAge || '0');
|
||||
bValue = parseInt(patientDataB.PatAge || '0');
|
||||
case 'processed':
|
||||
aValue = new Date(a.last_processed_at).getTime();
|
||||
bValue = new Date(b.last_processed_at).getTime();
|
||||
break;
|
||||
case 'date':
|
||||
default:
|
||||
aValue = new Date(a.created_at).getTime();
|
||||
bValue = new Date(b.created_at).getTime();
|
||||
aValue = new Date(a.patient_info.date).getTime();
|
||||
bValue = new Date(b.patient_info.date).getTime();
|
||||
break;
|
||||
}
|
||||
|
||||
@ -206,53 +190,68 @@ export const selectFilteredPatients = createSelector(
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Critical Patients
|
||||
* Select Processed Patients
|
||||
*
|
||||
* Purpose: Get patients with critical priority
|
||||
* Purpose: Get patients with processed status
|
||||
*/
|
||||
export const selectCriticalPatients = createSelector(
|
||||
export const selectProcessedPatients = createSelector(
|
||||
[selectPatients],
|
||||
(patients) => patients.filter(patient => patient.type === 'Critical')
|
||||
(patients) => {
|
||||
if (!patients || !Array.isArray(patients)) return [];
|
||||
return patients.filter((patient: PatientData) =>
|
||||
patient.patient_info.status.toLowerCase() === 'processed'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Active Patients
|
||||
* Select Pending Patients
|
||||
*
|
||||
* Purpose: Get patients with active status
|
||||
* Purpose: Get patients with pending status
|
||||
*/
|
||||
export const selectActivePatients = createSelector(
|
||||
export const selectPendingPatients = createSelector(
|
||||
[selectPatients],
|
||||
(patients: MedicalCase[]) => patients.filter((patient: MedicalCase) => {
|
||||
// Parse patient details to check status
|
||||
const parseJsonSafely = (jsonString: string | object) => {
|
||||
if (typeof jsonString === 'object') return jsonString;
|
||||
if (typeof jsonString === 'string') {
|
||||
try { return JSON.parse(jsonString); } catch { return {}; }
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
return patientData.Status === 'Active';
|
||||
})
|
||||
(patients) => {
|
||||
if (!patients || !Array.isArray(patients)) return [];
|
||||
return patients.filter((patient: PatientData) =>
|
||||
patient.patient_info.status.toLowerCase() === 'pending'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Patients by Department
|
||||
* Select Error Patients
|
||||
*
|
||||
* Purpose: Get patients grouped by department
|
||||
* Purpose: Get patients with error status
|
||||
*/
|
||||
export const selectPatientsByDepartment = createSelector(
|
||||
export const selectErrorPatients = createSelector(
|
||||
[selectPatients],
|
||||
(patients: MedicalCase[]) => {
|
||||
const grouped: { [key: string]: MedicalCase[] } = {};
|
||||
(patients) => {
|
||||
if (!patients || !Array.isArray(patients)) return [];
|
||||
return patients.filter((patient: PatientData) =>
|
||||
patient.patient_info.status.toLowerCase() === 'error'
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Patients by Modality
|
||||
*
|
||||
* Purpose: Get patients grouped by imaging modality
|
||||
*/
|
||||
export const selectPatientsByModality = createSelector(
|
||||
[selectPatients],
|
||||
(patients) => {
|
||||
if (!patients || !Array.isArray(patients)) return {};
|
||||
|
||||
patients.forEach((patient: MedicalCase) => {
|
||||
const dept = patient.type; // Use case type instead of department
|
||||
if (!grouped[dept]) {
|
||||
grouped[dept] = [];
|
||||
const grouped: { [key: string]: PatientData[] } = {};
|
||||
|
||||
patients.forEach((patient: PatientData) => {
|
||||
const modality = patient.patient_info.modality || 'Unknown';
|
||||
if (!grouped[modality]) {
|
||||
grouped[modality] = [];
|
||||
}
|
||||
grouped[dept].push(patient);
|
||||
grouped[modality].push(patient);
|
||||
});
|
||||
|
||||
return grouped;
|
||||
@ -266,43 +265,55 @@ export const selectPatientsByDepartment = createSelector(
|
||||
*/
|
||||
export const selectPatientStats = createSelector(
|
||||
[selectPatients],
|
||||
(patients: MedicalCase[]) => {
|
||||
(patients) => {
|
||||
if (!patients || !Array.isArray(patients)) {
|
||||
return {
|
||||
total: 0,
|
||||
processed: 0,
|
||||
pending: 0,
|
||||
error: 0,
|
||||
averageAge: 0,
|
||||
modalities: {},
|
||||
totalFiles: 0,
|
||||
processedPercentage: 0,
|
||||
pendingPercentage: 0,
|
||||
errorPercentage: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const total = patients.length;
|
||||
const critical = patients.filter((p: MedicalCase) => p.type === 'Critical').length;
|
||||
const emergency = patients.filter((p: MedicalCase) => p.type === 'Emergency').length;
|
||||
const routine = patients.filter((p: MedicalCase) => p.type === 'Routine').length;
|
||||
const processed = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'processed').length;
|
||||
const pending = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'pending').length;
|
||||
const error = patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'error').length;
|
||||
|
||||
// Parse patient details for age calculation
|
||||
const parseJsonSafely = (jsonString: string | object) => {
|
||||
if (typeof jsonString === 'object') return jsonString;
|
||||
if (typeof jsonString === 'string') {
|
||||
try { return JSON.parse(jsonString); } catch { return {}; }
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const totalAge = patients.reduce((sum: number, patient: MedicalCase) => {
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
return sum + parseInt(patientData.PatAge || '0');
|
||||
// Calculate average age
|
||||
const totalAge = patients.reduce((sum: number, patient: PatientData) => {
|
||||
const age = parseInt(patient.patient_info.age) || 0;
|
||||
return sum + age;
|
||||
}, 0);
|
||||
const averageAge = total > 0 ? Math.round(totalAge / total) : 0;
|
||||
|
||||
// Case type distribution
|
||||
const caseTypes: { [key: string]: number } = {};
|
||||
patients.forEach((patient: MedicalCase) => {
|
||||
caseTypes[patient.type] = (caseTypes[patient.type] || 0) + 1;
|
||||
// Modality distribution
|
||||
const modalities: { [key: string]: number } = {};
|
||||
patients.forEach((patient: PatientData) => {
|
||||
const modality = patient.patient_info.modality || 'Unknown';
|
||||
modalities[modality] = (modalities[modality] || 0) + 1;
|
||||
});
|
||||
|
||||
// Total files processed
|
||||
const totalFiles = patients.reduce((sum: number, patient: PatientData) => sum + (patient.total_files_processed || 0), 0);
|
||||
|
||||
return {
|
||||
total,
|
||||
critical,
|
||||
emergency,
|
||||
routine,
|
||||
processed,
|
||||
pending,
|
||||
error,
|
||||
averageAge,
|
||||
caseTypes,
|
||||
criticalPercentage: total > 0 ? Math.round((critical / total) * 100) : 0,
|
||||
emergencyPercentage: total > 0 ? Math.round((emergency / total) * 100) : 0,
|
||||
modalities,
|
||||
totalFiles,
|
||||
processedPercentage: total > 0 ? Math.round((processed / total) * 100) : 0,
|
||||
pendingPercentage: total > 0 ? Math.round((pending / total) * 100) : 0,
|
||||
errorPercentage: total > 0 ? Math.round((error / total) * 100) : 0,
|
||||
};
|
||||
}
|
||||
);
|
||||
@ -317,7 +328,10 @@ export const selectPatientStats = createSelector(
|
||||
export const selectPatientById = (patientId: string) =>
|
||||
createSelector(
|
||||
[selectPatients],
|
||||
(patients) => patients.find(patient => patient.id === patientId)
|
||||
(patients) => {
|
||||
if (!patients || !Array.isArray(patients)) return undefined;
|
||||
return patients.find((patient: PatientData) => patient.patid === patientId);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
@ -328,32 +342,20 @@ export const selectPatientById = (patientId: string) =>
|
||||
export const selectPatientsNeedAttention = createSelector(
|
||||
[selectPatients],
|
||||
(patients) => {
|
||||
return patients.filter(patient => {
|
||||
// Critical patients always need attention
|
||||
if (patient.priority === 'CRITICAL') return true;
|
||||
if (!patients || !Array.isArray(patients)) return [];
|
||||
|
||||
return patients.filter((patient: PatientData) => {
|
||||
// Error patients always need attention
|
||||
if (patient.patient_info.status.toLowerCase() === 'error') return true;
|
||||
|
||||
// Check vital signs for abnormal values
|
||||
const vitals = patient.vitalSigns;
|
||||
// Patients with critical report status
|
||||
if (patient.patient_info.report_status.toLowerCase() === 'critical') return true;
|
||||
|
||||
// Check blood pressure (hypertensive crisis)
|
||||
if (vitals.bloodPressure.systolic > 180 || vitals.bloodPressure.diastolic > 120) {
|
||||
return true;
|
||||
}
|
||||
// Patients with high frame count (complex cases)
|
||||
if (patient.patient_info.frame_count > 100) return true;
|
||||
|
||||
// Check heart rate (too high or too low)
|
||||
if (vitals.heartRate.value > 120 || vitals.heartRate.value < 50) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check temperature (fever or hypothermia)
|
||||
if (vitals.temperature.value > 38.5 || vitals.temperature.value < 35) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check oxygen saturation (low)
|
||||
if (vitals.oxygenSaturation.value < 90) {
|
||||
return true;
|
||||
}
|
||||
// Patients with multiple series (complex cases)
|
||||
if (patient.series_summary.length > 5) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
@ -367,7 +369,7 @@ export const selectPatientsNeedAttention = createSelector(
|
||||
*/
|
||||
export const selectHasPatientData = createSelector(
|
||||
[selectPatients],
|
||||
(patients) => patients.length > 0
|
||||
(patients) => patients && Array.isArray(patients) && patients.length > 0
|
||||
);
|
||||
|
||||
/**
|
||||
@ -378,7 +380,28 @@ export const selectHasPatientData = createSelector(
|
||||
export const selectIsEmptyState = createSelector(
|
||||
[selectPatients, selectPatientsLoading, selectFilteredPatients],
|
||||
(patients, isLoading, filteredPatients) =>
|
||||
!isLoading && patients.length > 0 && filteredPatients.length === 0
|
||||
!isLoading && patients && Array.isArray(patients) && patients.length > 0 && filteredPatients.length === 0
|
||||
);
|
||||
|
||||
/**
|
||||
* Select Patient Counts for Filters
|
||||
*
|
||||
* Purpose: Get patient counts for each filter category
|
||||
*/
|
||||
export const selectPatientCounts = createSelector(
|
||||
[selectPatients],
|
||||
(patients) => {
|
||||
if (!patients || !Array.isArray(patients)) {
|
||||
return { all: 0, processed: 0, pending: 0, error: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
all: patients.length,
|
||||
processed: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'processed').length,
|
||||
pending: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'pending').length,
|
||||
error: patients.filter((p: PatientData) => p.patient_info.status.toLowerCase() === 'error').length,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
|
||||
@ -6,9 +6,77 @@
|
||||
*/
|
||||
|
||||
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { MedicalCase, PatientCareState } from '../../../shared/types';
|
||||
import { patientAPI } from '../services/patientAPI';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* New API Response Types
|
||||
*/
|
||||
export interface SeriesSummary {
|
||||
series_num: string;
|
||||
series_description: string;
|
||||
total_images: number;
|
||||
png_preview: string;
|
||||
modality: string;
|
||||
}
|
||||
|
||||
export interface PatientInfo {
|
||||
name: string;
|
||||
age: string;
|
||||
sex: string;
|
||||
date: string;
|
||||
institution: string;
|
||||
modality: string;
|
||||
status: string;
|
||||
report_status: string;
|
||||
file_name: string;
|
||||
file_type: string;
|
||||
frame_count: number;
|
||||
}
|
||||
|
||||
export interface PatientData {
|
||||
patid: string;
|
||||
hospital_id: string;
|
||||
first_processed_at: string;
|
||||
last_processed_at: string;
|
||||
total_files_processed: number;
|
||||
patient_info: PatientInfo;
|
||||
series_summary: SeriesSummary[];
|
||||
processing_metadata: any;
|
||||
}
|
||||
|
||||
export interface PatientCareState {
|
||||
// Patients data
|
||||
patients: PatientData[];
|
||||
currentPatient: PatientData | null;
|
||||
|
||||
// Loading states
|
||||
isLoading: boolean;
|
||||
isRefreshing: boolean;
|
||||
isLoadingPatientDetails: boolean;
|
||||
|
||||
// Error handling
|
||||
error: string | null;
|
||||
|
||||
// Search and filtering
|
||||
searchQuery: string;
|
||||
selectedFilter: 'all' | 'processed' | 'pending' | 'error';
|
||||
sortBy: 'date' | 'name' | 'processed';
|
||||
sortOrder: 'asc' | 'desc';
|
||||
|
||||
// Pagination
|
||||
currentPage: number;
|
||||
itemsPerPage: number;
|
||||
totalItems: number;
|
||||
|
||||
// Cache
|
||||
lastUpdated: string | null;
|
||||
cacheExpiry: string | null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ASYNC THUNKS
|
||||
// ============================================================================
|
||||
@ -24,84 +92,78 @@ export const fetchPatients = createAsyncThunk(
|
||||
'patientCare/fetchPatients',
|
||||
async (token: string, { rejectWithValue }) => {
|
||||
try {
|
||||
// Make actual API call to fetch medical cases
|
||||
const response :any = await patientAPI.getPatients(token);
|
||||
if (response.ok && response.data&&response.data.success) {
|
||||
// Add random case types to each patient record
|
||||
const caseTypes: Array<'Critical' | 'Emergency' | 'Routine'> = ['Critical', 'Emergency', 'Routine'];
|
||||
|
||||
const patientsWithTypes = response.data.data.map((patient: any) => ({
|
||||
...patient,
|
||||
type: caseTypes[Math.floor(Math.random() * caseTypes.length)]
|
||||
}));
|
||||
|
||||
return patientsWithTypes as MedicalCase[];
|
||||
const response: any = await patientAPI.getPatients(token);
|
||||
console.log('response', response);
|
||||
|
||||
if (response.ok && response.data&& response.data.data) {
|
||||
// Return the patients data directly from the new API structure
|
||||
return response.data.data as PatientData[];
|
||||
} else {
|
||||
// Fallback to mock data for development
|
||||
const mockPatients: MedicalCase[] = [
|
||||
const mockPatients: PatientData[] = [
|
||||
{
|
||||
id: 1,
|
||||
patientdetails: JSON.stringify({
|
||||
patientdetails: {
|
||||
Date: '2024-01-15',
|
||||
Name: 'John Doe',
|
||||
PatID: 'MRN001',
|
||||
PatAge: '38',
|
||||
PatSex: 'M',
|
||||
Status: 'Active',
|
||||
InstName: 'City General Hospital',
|
||||
Modality: 'CT',
|
||||
ReportStatus: 'Pending'
|
||||
}
|
||||
}),
|
||||
series: JSON.stringify([
|
||||
patid: "demo001",
|
||||
hospital_id: "demo-hospital-001",
|
||||
first_processed_at: "2025-01-15T10:30:00Z",
|
||||
last_processed_at: "2025-01-15T11:45:00Z",
|
||||
total_files_processed: 3,
|
||||
patient_info: {
|
||||
name: "John Doe",
|
||||
age: "38",
|
||||
sex: "M",
|
||||
date: "2025-01-15",
|
||||
institution: "City General Hospital",
|
||||
modality: "CT",
|
||||
status: "Processed",
|
||||
report_status: "Available",
|
||||
file_name: "chest_ct_001.dcm",
|
||||
file_type: "dcm",
|
||||
frame_count: 50
|
||||
},
|
||||
series_summary: [
|
||||
{
|
||||
Path: ['/dicom/series1'],
|
||||
SerDes: 'Chest CT',
|
||||
ViePos: 'Supine',
|
||||
pngpath: '/images/ct_chest_1.png',
|
||||
SeriesNum: '1',
|
||||
ImgTotalinSeries: '50'
|
||||
series_num: "1",
|
||||
series_description: "Chest CT",
|
||||
total_images: 50,
|
||||
png_preview: "/images/ct_chest_1.png",
|
||||
modality: "CT"
|
||||
}
|
||||
]),
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
updated_at: '2024-01-15T11:45:00Z',
|
||||
series_id: 'series_001',
|
||||
type: 'Critical'
|
||||
],
|
||||
processing_metadata: {}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
patientdetails: JSON.stringify({
|
||||
patientdetails: {
|
||||
Date: '2024-01-15',
|
||||
Name: 'Jane Smith',
|
||||
PatID: 'MRN002',
|
||||
PatAge: '33',
|
||||
PatSex: 'F',
|
||||
Status: 'Active',
|
||||
InstName: 'Memorial Medical Center',
|
||||
Modality: 'MR',
|
||||
ReportStatus: 'Completed'
|
||||
}
|
||||
}),
|
||||
series: JSON.stringify([
|
||||
patid: "demo002",
|
||||
hospital_id: "demo-hospital-002",
|
||||
first_processed_at: "2025-01-15T09:15:00Z",
|
||||
last_processed_at: "2025-01-15T10:30:00Z",
|
||||
total_files_processed: 2,
|
||||
patient_info: {
|
||||
name: "Jane Smith",
|
||||
age: "33",
|
||||
sex: "F",
|
||||
date: "2025-01-15",
|
||||
institution: "Memorial Medical Center",
|
||||
modality: "MR",
|
||||
status: "Processed",
|
||||
report_status: "Available",
|
||||
file_name: "brain_mri_001.dcm",
|
||||
file_type: "dcm",
|
||||
frame_count: 120
|
||||
},
|
||||
series_summary: [
|
||||
{
|
||||
Path: ['/dicom/series2'],
|
||||
SerDes: 'Brain MRI',
|
||||
ViePos: 'Supine',
|
||||
pngpath: '/images/mri_brain_1.png',
|
||||
SeriesNum: '2',
|
||||
ImgTotalinSeries: '120'
|
||||
series_num: "1",
|
||||
series_description: "Brain MRI",
|
||||
total_images: 120,
|
||||
png_preview: "/images/mri_brain_1.png",
|
||||
modality: "MR"
|
||||
}
|
||||
]),
|
||||
created_at: '2024-01-15T09:15:00Z',
|
||||
updated_at: '2024-01-15T10:30:00Z',
|
||||
series_id: 'series_002',
|
||||
type: 'Routine'
|
||||
},
|
||||
],
|
||||
processing_metadata: {}
|
||||
}
|
||||
];
|
||||
|
||||
return mockPatients;
|
||||
return [];
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Fetch patients error:', error);
|
||||
@ -126,35 +188,35 @@ export const fetchPatientDetails = createAsyncThunk(
|
||||
await new Promise((resolve) => setTimeout(resolve as any, 1000));
|
||||
|
||||
// Mock patient details for specific patient
|
||||
const mockPatient: MedicalCase = {
|
||||
id: parseInt(patientId),
|
||||
patientdetails: JSON.stringify({
|
||||
patientdetails: {
|
||||
Date: '2024-01-15',
|
||||
Name: 'John Doe',
|
||||
PatID: `MRN${patientId.padStart(3, '0')}`,
|
||||
PatAge: '38',
|
||||
PatSex: 'M',
|
||||
Status: 'Active',
|
||||
InstName: 'City General Hospital',
|
||||
Modality: 'CT',
|
||||
ReportStatus: 'Pending'
|
||||
}
|
||||
}),
|
||||
series: JSON.stringify([
|
||||
const mockPatient: PatientData = {
|
||||
patid: patientId,
|
||||
hospital_id: `demo-hospital-${patientId}`,
|
||||
first_processed_at: "2025-01-15T10:30:00Z",
|
||||
last_processed_at: "2025-01-15T11:45:00Z",
|
||||
total_files_processed: 3,
|
||||
patient_info: {
|
||||
name: `Patient ${patientId}`,
|
||||
age: "38",
|
||||
sex: "M",
|
||||
date: "2025-01-15",
|
||||
institution: "City General Hospital",
|
||||
modality: "CT",
|
||||
status: "Processed",
|
||||
report_status: "Available",
|
||||
file_name: `patient_${patientId}.dcm`,
|
||||
file_type: "dcm",
|
||||
frame_count: 50
|
||||
},
|
||||
series_summary: [
|
||||
{
|
||||
Path: [`/dicom/series${patientId}`],
|
||||
SerDes: 'Chest CT',
|
||||
ViePos: 'Supine',
|
||||
pngpath: `/images/ct_chest_${patientId}.png`,
|
||||
SeriesNum: patientId,
|
||||
ImgTotalinSeries: '50'
|
||||
series_num: "1",
|
||||
series_description: "Chest CT",
|
||||
total_images: 50,
|
||||
png_preview: `/images/ct_chest_${patientId}.png`,
|
||||
modality: "CT"
|
||||
}
|
||||
]),
|
||||
created_at: '2024-01-15T10:30:00Z',
|
||||
updated_at: '2024-01-15T11:45:00Z',
|
||||
series_id: `series_${patientId.padStart(3, '0')}`,
|
||||
type: 'Critical'
|
||||
],
|
||||
processing_metadata: {}
|
||||
};
|
||||
|
||||
return mockPatient;
|
||||
@ -174,7 +236,7 @@ export const fetchPatientDetails = createAsyncThunk(
|
||||
*/
|
||||
export const updatePatient = createAsyncThunk(
|
||||
'patientCare/updatePatient',
|
||||
async (patientData: Partial<MedicalCase> & { id: number }, { rejectWithValue }) => {
|
||||
async (patientData: Partial<PatientData> & { patid: string }, { rejectWithValue }) => {
|
||||
try {
|
||||
// TODO: Replace with actual API call
|
||||
await new Promise((resolve) => setTimeout(resolve as any, 800));
|
||||
@ -200,6 +262,7 @@ export const updatePatient = createAsyncThunk(
|
||||
* - Loading states for async operations
|
||||
* - Error handling and messages
|
||||
* - Search and filtering
|
||||
* - Pagination and caching
|
||||
*/
|
||||
const initialState: PatientCareState = {
|
||||
// Patients data
|
||||
@ -275,7 +338,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Set patient filter
|
||||
*/
|
||||
setFilter: (state, action: PayloadAction<'all' | 'Critical' | 'Routine' | 'Emergency'>) => {
|
||||
setFilter: (state, action: PayloadAction<'all' | 'processed' | 'pending' | 'error'>) => {
|
||||
state.selectedFilter = action.payload;
|
||||
state.currentPage = 1; // Reset to first page when filtering
|
||||
},
|
||||
@ -285,7 +348,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Set patient sort options
|
||||
*/
|
||||
setSort: (state, action: PayloadAction<{ by: 'date' | 'name' | 'age'; order: 'asc' | 'desc' }>) => {
|
||||
setSort: (state, action: PayloadAction<{ by: 'date' | 'name' | 'processed'; order: 'asc' | 'desc' }>) => {
|
||||
state.sortBy = action.payload.by;
|
||||
state.sortOrder = action.payload.order;
|
||||
},
|
||||
@ -314,7 +377,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Set the currently selected patient
|
||||
*/
|
||||
setCurrentPatient: (state, action: PayloadAction<MedicalCase | null>) => {
|
||||
setCurrentPatient: (state, action: PayloadAction<PatientData | null>) => {
|
||||
state.currentPatient = action.payload;
|
||||
},
|
||||
|
||||
@ -323,14 +386,14 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Update a patient in the patients list
|
||||
*/
|
||||
updatePatientInList: (state, action: PayloadAction<MedicalCase>) => {
|
||||
const index = state.patients.findIndex(patient => patient.id === action.payload.id);
|
||||
updatePatientInList: (state, action: PayloadAction<PatientData>) => {
|
||||
const index = state.patients.findIndex(patient => patient.patid === action.payload.patid);
|
||||
if (index !== -1) {
|
||||
state.patients[index] = action.payload;
|
||||
}
|
||||
|
||||
// Update current patient if it's the same patient
|
||||
if (state.currentPatient && state.currentPatient.id === action.payload.id) {
|
||||
if (state.currentPatient && state.currentPatient.patid === action.payload.patid) {
|
||||
state.currentPatient = action.payload;
|
||||
}
|
||||
},
|
||||
@ -340,7 +403,7 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Add a new patient to the list
|
||||
*/
|
||||
addPatient: (state, action: PayloadAction<MedicalCase>) => {
|
||||
addPatient: (state, action: PayloadAction<PatientData>) => {
|
||||
state.patients.unshift(action.payload);
|
||||
state.totalItems += 1;
|
||||
},
|
||||
@ -350,15 +413,15 @@ const patientCareSlice = createSlice({
|
||||
*
|
||||
* Purpose: Remove a patient from the list
|
||||
*/
|
||||
removePatient: (state, action: PayloadAction<number>) => {
|
||||
const index = state.patients.findIndex(patient => patient.id === action.payload);
|
||||
removePatient: (state, action: PayloadAction<string>) => {
|
||||
const index = state.patients.findIndex(patient => patient.patid === action.payload);
|
||||
if (index !== -1) {
|
||||
state.patients.splice(index, 1);
|
||||
state.totalItems -= 1;
|
||||
}
|
||||
|
||||
// Clear current patient if it's the same patient
|
||||
if (state.currentPatient && state.currentPatient.id === action.payload) {
|
||||
if (state.currentPatient && state.currentPatient.patid === action.payload) {
|
||||
state.currentPatient = null;
|
||||
}
|
||||
},
|
||||
@ -415,13 +478,13 @@ const patientCareSlice = createSlice({
|
||||
builder
|
||||
.addCase(updatePatient.fulfilled, (state, action) => {
|
||||
// Update patient in list
|
||||
const index = state.patients.findIndex(patient => patient.id === action.payload.id);
|
||||
const index = state.patients.findIndex(patient => patient.patid === action.payload.patid);
|
||||
if (index !== -1) {
|
||||
state.patients[index] = { ...state.patients[index], ...action.payload };
|
||||
}
|
||||
|
||||
// Update current patient if it's the same patient
|
||||
if (state.currentPatient && state.currentPatient.id === action.payload.id) {
|
||||
if (state.currentPatient && state.currentPatient.patid === action.payload.patid) {
|
||||
state.currentPatient = { ...state.currentPatient, ...action.payload };
|
||||
}
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,61 +5,49 @@
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
RefreshControl,
|
||||
TouchableOpacity,
|
||||
StatusBar,
|
||||
Alert,
|
||||
FlatList,
|
||||
Dimensions,
|
||||
RefreshControl,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { theme } from '../../../theme/theme';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { theme } from '../../../theme/theme';
|
||||
|
||||
// Import patient care functionality
|
||||
import {
|
||||
fetchPatients,
|
||||
setSearchQuery,
|
||||
setFilter,
|
||||
setSort,
|
||||
clearError
|
||||
} from '../redux/patientCareSlice';
|
||||
|
||||
// Import patient care selectors
|
||||
import {
|
||||
selectPatients,
|
||||
selectPatientsLoading,
|
||||
selectPatientsError,
|
||||
selectIsRefreshing,
|
||||
selectSearchQuery,
|
||||
selectSelectedFilter,
|
||||
selectSortBy,
|
||||
selectFilteredPatients,
|
||||
} from '../redux/patientCareSelectors';
|
||||
|
||||
// Import auth selectors
|
||||
import { selectUser } from '../../Auth/redux/authSelectors';
|
||||
|
||||
// Import components
|
||||
// Components
|
||||
import PatientCard from '../components/PatientCard';
|
||||
import SearchBar from '../components/SearchBar';
|
||||
import FilterTabs from '../components/FilterTabs';
|
||||
import EmptyState from '../components/EmptyState';
|
||||
import LoadingState from '../components/LoadingState';
|
||||
import EmptyState from '../components/EmptyState';
|
||||
|
||||
// Import types
|
||||
import { MedicalCase, PatientDetails, Series } from '../../../shared/types';
|
||||
import { PatientsScreenProps } from '../navigation/navigationTypes';
|
||||
// Redux
|
||||
import {
|
||||
fetchPatients,
|
||||
setSearchQuery,
|
||||
setFilter,
|
||||
} from '../redux/patientCareSlice';
|
||||
import {
|
||||
selectPatients,
|
||||
selectFilteredPatients,
|
||||
selectPatientsLoading,
|
||||
selectIsRefreshing,
|
||||
selectPatientsError,
|
||||
selectSearchQuery,
|
||||
selectSelectedFilter,
|
||||
selectPatientCounts,
|
||||
} from '../redux/patientCareSelectors';
|
||||
|
||||
// Get screen dimensions
|
||||
const { width: screenWidth } = Dimensions.get('window');
|
||||
// Types
|
||||
import { PatientData } from '../redux/patientCareSlice';
|
||||
import { selectUser } from '../../Auth/redux/authSelectors';
|
||||
|
||||
// ============================================================================
|
||||
// INTERFACES
|
||||
@ -77,8 +65,8 @@ const { width: screenWidth } = Dimensions.get('window');
|
||||
* Features:
|
||||
* - Real-time patient data fetching
|
||||
* - Search functionality with real-time filtering
|
||||
* - Filter tabs (All, Active, Critical, Discharged)
|
||||
* - Sort options (Priority, Name, Date)
|
||||
* - Filter tabs (All, Processed, Pending, Error)
|
||||
* - Sort options (Date, Name, Processed)
|
||||
* - Pull-to-refresh functionality
|
||||
* - Patient cards with vital information
|
||||
* - Navigation to patient details
|
||||
@ -86,12 +74,13 @@ const { width: screenWidth } = Dimensions.get('window');
|
||||
* - Empty state handling
|
||||
* - Modern ER-focused UI design
|
||||
*/
|
||||
const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
const PatientsScreen: React.FC = () => {
|
||||
// ============================================================================
|
||||
// STATE MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const navigation = useNavigation();
|
||||
|
||||
// Redux state
|
||||
const patients = useAppSelector(selectPatients);
|
||||
@ -101,85 +90,56 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
const error = useAppSelector(selectPatientsError);
|
||||
const searchQuery = useAppSelector(selectSearchQuery);
|
||||
const selectedFilter = useAppSelector(selectSelectedFilter);
|
||||
const sortBy = useAppSelector(selectSortBy);
|
||||
const patientCounts = useAppSelector(selectPatientCounts);
|
||||
|
||||
// Auth state
|
||||
const user = useAppSelector(selectUser);
|
||||
|
||||
// Local state
|
||||
const [showSortModal, setShowSortModal] = useState(false);
|
||||
|
||||
// ============================================================================
|
||||
// LIFECYCLE METHODS
|
||||
// EFFECTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Component Mount Effect
|
||||
* Fetch Patients on Mount
|
||||
*
|
||||
* Purpose: Initialize screen and fetch patient data
|
||||
* Purpose: Load patients when component mounts
|
||||
*/
|
||||
useEffect(() => {
|
||||
// Fetch patients on mount
|
||||
handleFetchPatients();
|
||||
|
||||
// Set up navigation focus listener for real-time updates
|
||||
const unsubscribe = navigation.addListener('focus', () => {
|
||||
handleRefresh();
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [navigation]);
|
||||
|
||||
/**
|
||||
* Error Handling Effect
|
||||
*
|
||||
* Purpose: Display error alerts and clear errors
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
Alert.alert(
|
||||
'Error',
|
||||
error,
|
||||
[
|
||||
{
|
||||
text: 'Retry',
|
||||
onPress: handleFetchPatients,
|
||||
},
|
||||
{
|
||||
text: 'OK',
|
||||
onPress: () => dispatch(clearError()),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle Fetch Patients
|
||||
*
|
||||
* Purpose: Fetch patients from API
|
||||
*/
|
||||
const handleFetchPatients = useCallback(() => {
|
||||
if (user?.access_token) {
|
||||
dispatch(fetchPatients(user.access_token));
|
||||
}
|
||||
}, [dispatch, user?.access_token]);
|
||||
|
||||
/**
|
||||
* Clear Error on Unmount
|
||||
*
|
||||
* Purpose: Clean up error state when component unmounts
|
||||
*/
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// No clearError action in this file, so this effect is removed.
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle Refresh
|
||||
*
|
||||
* Purpose: Pull-to-refresh functionality
|
||||
* Purpose: Handle pull-to-refresh functionality
|
||||
*/
|
||||
const handleRefresh = useCallback(() => {
|
||||
handleFetchPatients();
|
||||
}, [handleFetchPatients]);
|
||||
if (user?.access_token) {
|
||||
dispatch(fetchPatients(user.access_token));
|
||||
}
|
||||
}, [dispatch, user?.access_token]);
|
||||
|
||||
/**
|
||||
* Handle Search
|
||||
*
|
||||
* Purpose: Handle search input changes
|
||||
* Purpose: Handle search query changes
|
||||
*
|
||||
* @param query - Search query string
|
||||
*/
|
||||
@ -190,106 +150,36 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
/**
|
||||
* Handle Filter Change
|
||||
*
|
||||
* Purpose: Handle filter tab selection
|
||||
*
|
||||
* @param filter - Selected filter option
|
||||
* Purpose: Update the selected filter and refresh the list
|
||||
*/
|
||||
const handleFilterChange = useCallback((filter: 'all' | 'Critical' | 'Routine' | 'Emergency') => {
|
||||
const handleFilterChange = useCallback((filter: 'all' | 'processed' | 'pending' | 'error') => {
|
||||
dispatch(setFilter(filter));
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Handle Sort Change
|
||||
*
|
||||
* Purpose: Handle sort option selection
|
||||
*
|
||||
* @param sortOption - Selected sort option
|
||||
*/
|
||||
const handleSortChange = useCallback((sortOption: 'date' | 'name' | 'age') => {
|
||||
dispatch(setSort({ by: sortOption, order: 'desc' }));
|
||||
setShowSortModal(false);
|
||||
}, [dispatch]);
|
||||
|
||||
/**
|
||||
* Handle Patient Press
|
||||
*
|
||||
* Purpose: Navigate to patient details screen
|
||||
*
|
||||
* @param patient - Selected patient
|
||||
* Purpose: Navigate to patient details when a patient card is pressed
|
||||
*/
|
||||
const handlePatientPress = useCallback((patient: MedicalCase) => {
|
||||
// Helper function to parse JSON strings safely
|
||||
const parseJsonSafely = (jsonString: string | object) => {
|
||||
if (typeof jsonString === 'object') {
|
||||
return jsonString;
|
||||
}
|
||||
if (typeof jsonString === 'string') {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse JSON:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
|
||||
navigation.navigate('PatientDetails', {
|
||||
patientId:'1',
|
||||
patientName: patientData.Name || 'Unknown Patient',
|
||||
medicalCase: patient,
|
||||
const handlePatientPress = useCallback((patient: PatientData) => {
|
||||
(navigation as any).navigate('PatientDetails', {
|
||||
patientId: patient.patid,
|
||||
patientName: patient.patient_info.name,
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
/**
|
||||
* Handle Emergency Alert
|
||||
*
|
||||
* Purpose: Handle emergency alert for critical patients
|
||||
*
|
||||
* @param patient - Patient with emergency
|
||||
* Purpose: Show emergency alert for critical patients
|
||||
*/
|
||||
const handleEmergencyAlert = useCallback((patient: MedicalCase) => {
|
||||
// Helper function to parse JSON strings safely
|
||||
const parseJsonSafely = (jsonString: string | object) => {
|
||||
if (typeof jsonString === 'object') {
|
||||
return jsonString;
|
||||
}
|
||||
if (typeof jsonString === 'string') {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse JSON:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const patientDetails = parseJsonSafely(patient.patientdetails);
|
||||
const patientData = patientDetails.patientdetails || patientDetails;
|
||||
|
||||
const handleEmergencyAlert = useCallback((patient: PatientData) => {
|
||||
Alert.alert(
|
||||
'Emergency Alert',
|
||||
`Critical status for ${patientData.Name || 'Unknown Patient'}\nID: ${patientData.PatID || 'N/A'}`,
|
||||
`Patient ${patient.patient_info.name} (ID: ${patient.patid}) requires immediate attention!\n\nStatus: ${patient.patient_info.report_status}\nPriority: ${patient.patient_info.status}`,
|
||||
[
|
||||
{
|
||||
text: 'View Details',
|
||||
onPress: () => handlePatientPress(patient),
|
||||
},
|
||||
{
|
||||
text: 'Call Physician',
|
||||
onPress: () => {
|
||||
// TODO: Implement physician calling functionality
|
||||
Alert.alert('Calling', `Calling attending physician...`);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Cancel',
|
||||
style: 'cancel',
|
||||
},
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{ text: 'View Details', onPress: () => handlePatientPress(patient) },
|
||||
]
|
||||
);
|
||||
}, [handlePatientPress]);
|
||||
@ -299,18 +189,52 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Render Patient Item
|
||||
* Render Patient Card
|
||||
*
|
||||
* Purpose: Render individual patient card
|
||||
*
|
||||
* @param item - Patient data with render info
|
||||
* Purpose: Render individual patient card component
|
||||
*/
|
||||
const renderPatientItem = ({ item }: { item: MedicalCase }) => (
|
||||
const renderPatientCard = useCallback(({ item }: { item: PatientData }) => (
|
||||
<PatientCard
|
||||
patient={item}
|
||||
onPress={() => handlePatientPress(item)}
|
||||
onEmergencyPress={() => handleEmergencyAlert(item)}
|
||||
/>
|
||||
), [handlePatientPress, handleEmergencyAlert]);
|
||||
|
||||
/**
|
||||
* Render Header
|
||||
*
|
||||
* Purpose: Render the screen header with title and action buttons
|
||||
*/
|
||||
const renderHeader = () => (
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<Text style={styles.headerTitle}>Patients</Text>
|
||||
<Text style={styles.headerSubtitle}>
|
||||
{filteredPatients.length} of {patients?.length || 0} patients
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.headerRight}>
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={() => {
|
||||
// TODO: Implement sort modal
|
||||
}}
|
||||
>
|
||||
<Text style={styles.actionButtonText}>Sort</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.actionButton}
|
||||
onPress={() => {
|
||||
// TODO: Implement filter modal
|
||||
}}
|
||||
>
|
||||
<Text style={styles.actionButtonText}>Filter</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
/**
|
||||
@ -318,168 +242,143 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
*
|
||||
* Purpose: Render empty state when no patients found
|
||||
*/
|
||||
const renderEmptyState = () => {
|
||||
if (isLoading) return null;
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
title={searchQuery ? 'No patients found' : 'No patients available'}
|
||||
subtitle={
|
||||
searchQuery
|
||||
? `No patients match "${searchQuery}"`
|
||||
: 'Patients will appear here when available'
|
||||
}
|
||||
iconName="users"
|
||||
onRetry={searchQuery ? undefined : handleFetchPatients}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Render Loading State
|
||||
*
|
||||
* Purpose: Render loading state during initial fetch
|
||||
*/
|
||||
if (isLoading && patients.length === 0) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
||||
<LoadingState
|
||||
title="Loading Patients"
|
||||
subtitle="Fetching patient data from server..."
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
const renderEmptyState = () => (
|
||||
<EmptyState
|
||||
title="No Patients Found"
|
||||
subtitle={searchQuery.trim() ?
|
||||
`No patients match "${searchQuery}"` :
|
||||
"No patients available at the moment"
|
||||
}
|
||||
iconName="users"
|
||||
onRetry={handleRefresh}
|
||||
retryText="Refresh"
|
||||
/>
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// MAIN RENDER
|
||||
// ============================================================================
|
||||
|
||||
if (error && !isLoading) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorTitle}>Error Loading Patients</Text>
|
||||
<Text style={styles.errorMessage}>{error}</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.retryButton}
|
||||
onPress={handleRefresh}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.retryButtonText}>Retry</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
||||
|
||||
{/* Fixed Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={() => navigation.goBack()}
|
||||
>
|
||||
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<View>
|
||||
<Text style={styles.headerTitle}>Patients</Text>
|
||||
<Text style={styles.headerSubtitle}>Emergency Department</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* Header */}
|
||||
{renderHeader()}
|
||||
|
||||
{/* Search and Filters */}
|
||||
<View style={styles.searchAndFilters}>
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
placeholder="Search patients, ID, institution..."
|
||||
/>
|
||||
|
||||
<View style={styles.headerRight}>
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={handleRefresh}
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<Icon
|
||||
name="refresh-cw"
|
||||
size={20}
|
||||
color={isRefreshing ? theme.colors.textMuted : theme.colors.primary}
|
||||
<FilterTabs
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
patientCounts={patientCounts}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Loading State */}
|
||||
{isLoading && patients.length === 0 && (
|
||||
<View style={styles.centerContainer}>
|
||||
<LoadingState />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{error && patients.length === 0 && (
|
||||
<View style={styles.centerContainer}>
|
||||
<EmptyState
|
||||
iconName="alert-circle"
|
||||
title="Error Loading Patients"
|
||||
subtitle={error}
|
||||
retryText="Retry"
|
||||
onRetry={handleRefresh}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Empty State */}
|
||||
{!isLoading && !error && patients.length === 0 && (
|
||||
<View style={styles.centerContainer}>
|
||||
<EmptyState
|
||||
iconName="users"
|
||||
title="No Patients Found"
|
||||
subtitle="There are no patients in the system yet."
|
||||
retryText="Refresh"
|
||||
onRetry={handleRefresh}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* No Results State */}
|
||||
{!isLoading && !error && patients.length > 0 && filteredPatients.length === 0 && (
|
||||
<View style={styles.centerContainer}>
|
||||
<EmptyState
|
||||
iconName="search"
|
||||
title="No Results Found"
|
||||
subtitle={`No patients match your search "${searchQuery}" and filter "${selectedFilter}"`}
|
||||
retryText="Clear Search"
|
||||
onRetry={() => {
|
||||
handleSearch('');
|
||||
handleFilterChange('all');
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Patient List */}
|
||||
{!isLoading && !error && filteredPatients.length > 0 && (
|
||||
<FlatList
|
||||
data={filteredPatients}
|
||||
renderItem={renderPatientCard}
|
||||
keyExtractor={(item) => item.patid}
|
||||
contentContainerStyle={styles.listContainer}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
colors={[theme.colors.primary]}
|
||||
tintColor={theme.colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.headerButton}
|
||||
onPress={() => {
|
||||
// TODO: Implement notifications screen
|
||||
Alert.alert('Notifications', 'Notifications feature coming soon');
|
||||
}}
|
||||
>
|
||||
<Icon name="bell" size={20} color={theme.colors.textSecondary} />
|
||||
{/* Notification badge */}
|
||||
<View style={styles.notificationBadge}>
|
||||
<Text style={styles.badgeText}>3</Text>
|
||||
}
|
||||
ListFooterComponent={
|
||||
<View style={styles.listFooter}>
|
||||
<Text style={styles.footerText}>
|
||||
Showing {filteredPatients.length} of {patients.length} patients
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Fixed Search and Filter Section */}
|
||||
<View style={styles.fixedSection}>
|
||||
{/* Search Bar */}
|
||||
<View style={styles.searchContainer}>
|
||||
<SearchBar
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
placeholder="Search patients by name, MRN, or room..."
|
||||
showFilter
|
||||
onFilterPress={() => setShowSortModal(true)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<View style={styles.filterContainer}>
|
||||
<FilterTabs
|
||||
selectedFilter={selectedFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
patientCounts={{
|
||||
all: patients.length,
|
||||
Critical: patients.filter((p: MedicalCase) => p.type === 'Critical').length,
|
||||
Routine: patients.filter((p: MedicalCase) => p.type === 'Routine').length,
|
||||
Emergency: patients.filter((p: MedicalCase) => p.type === 'Emergency').length,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Results Summary */}
|
||||
<View style={styles.resultsSummary}>
|
||||
<View style={styles.resultsLeft}>
|
||||
<Icon name="users" size={16} color={theme.colors.textSecondary} />
|
||||
<Text style={styles.resultsText}>
|
||||
{filteredPatients.length} patient{filteredPatients.length !== 1 ? 's' : ''} found
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.sortInfo}>
|
||||
<Icon name="filter" size={14} color={theme.colors.textMuted} />
|
||||
<Text style={styles.sortText}>
|
||||
Sorted by {sortBy}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Scrollable Patient List Only */}
|
||||
<FlatList
|
||||
data={filteredPatients}
|
||||
renderItem={renderPatientItem}
|
||||
keyExtractor={(item,index) => index.toString()}
|
||||
ListEmptyComponent={renderEmptyState}
|
||||
contentContainerStyle={[
|
||||
styles.listContent,
|
||||
filteredPatients.length === 0 && styles.emptyListContent
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
colors={[theme.colors.primary]}
|
||||
tintColor={theme.colors.primary}
|
||||
/>
|
||||
}
|
||||
// Performance optimizations
|
||||
// removeClippedSubviews={true}
|
||||
// maxToRenderPerBatch={10}
|
||||
// windowSize={10}
|
||||
// initialNumToRender={8}
|
||||
// getItemLayout={(data, index) => ({
|
||||
// length: 120, // Approximate height of PatientCard
|
||||
// offset: 120 * index,
|
||||
// index,
|
||||
// })}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* TODO: Implement sort and filter modals for enhanced functionality */}
|
||||
|
||||
{/* Note: Patient data will be loaded from API when fetchPatients is called */}
|
||||
{/* Currently using mock data from Redux slice for development */}
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
@ -489,6 +388,7 @@ const PatientsScreen: React.FC<PatientsScreenProps> = ({ navigation }) => {
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
// Container Styles
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.background,
|
||||
@ -500,19 +400,17 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
paddingVertical: theme.spacing.md,
|
||||
backgroundColor: theme.colors.background,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
headerLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
backButton: {
|
||||
marginRight: theme.spacing.sm,
|
||||
padding: theme.spacing.xs,
|
||||
headerRight: {
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing.sm,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 24,
|
||||
@ -523,89 +421,89 @@ const styles = StyleSheet.create({
|
||||
headerSubtitle: {
|
||||
fontSize: 14,
|
||||
color: theme.colors.textSecondary,
|
||||
fontFamily: theme.typography.fontFamily.bold,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
},
|
||||
headerRight: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
headerButton: {
|
||||
padding: theme.spacing.sm,
|
||||
marginLeft: theme.spacing.xs,
|
||||
position: 'relative',
|
||||
},
|
||||
notificationBadge: {
|
||||
position: 'absolute',
|
||||
top: 6,
|
||||
right: 6,
|
||||
backgroundColor: theme.colors.error,
|
||||
actionButton: {
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
borderRadius: 8,
|
||||
width: 16,
|
||||
height: 16,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
badgeText: {
|
||||
color: theme.colors.background,
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
actionButtonText: {
|
||||
color: theme.colors.textSecondary,
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
fontFamily: theme.typography.fontFamily.medium,
|
||||
},
|
||||
|
||||
// Fixed Section Styles
|
||||
fixedSection: {
|
||||
// Search and Filters
|
||||
searchAndFilters: {
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingTop: theme.spacing.sm,
|
||||
paddingBottom: theme.spacing.md,
|
||||
paddingBottom: theme.spacing.sm,
|
||||
backgroundColor: theme.colors.background,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
searchContainer: {
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
filterContainer: {
|
||||
marginBottom: theme.spacing.sm,
|
||||
|
||||
// Center Container for States
|
||||
centerContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: theme.spacing.md,
|
||||
},
|
||||
|
||||
// List Styles
|
||||
listContent: {
|
||||
paddingTop: theme.spacing.sm,
|
||||
paddingBottom: theme.spacing.xl,
|
||||
listContainer: {
|
||||
paddingBottom: theme.spacing.lg,
|
||||
},
|
||||
emptyListContent: {
|
||||
flexGrow: 1,
|
||||
listFooter: {
|
||||
paddingVertical: theme.spacing.md,
|
||||
alignItems: 'center',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 14,
|
||||
color: theme.colors.textMuted,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
},
|
||||
|
||||
// Results Summary
|
||||
resultsSummary: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
// Error State Styles
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingVertical: theme.spacing.sm,
|
||||
paddingHorizontal: theme.spacing.sm,
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
borderRadius: 8,
|
||||
marginTop: theme.spacing.xs,
|
||||
padding: theme.spacing.xl,
|
||||
},
|
||||
resultsLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
errorTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: theme.colors.error,
|
||||
marginBottom: theme.spacing.sm,
|
||||
fontFamily: theme.typography.fontFamily.bold,
|
||||
textAlign: 'center',
|
||||
},
|
||||
resultsText: {
|
||||
fontSize: 14,
|
||||
color: theme.colors.textPrimary,
|
||||
fontWeight: '500',
|
||||
marginLeft: theme.spacing.xs,
|
||||
},
|
||||
sortInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
sortText: {
|
||||
fontSize: 12,
|
||||
errorMessage: {
|
||||
fontSize: 16,
|
||||
color: theme.colors.textSecondary,
|
||||
textTransform: 'capitalize',
|
||||
marginLeft: theme.spacing.xs,
|
||||
marginBottom: theme.spacing.lg,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
textAlign: 'center',
|
||||
},
|
||||
retryButton: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
paddingHorizontal: theme.spacing.lg,
|
||||
paddingVertical: theme.spacing.md,
|
||||
borderRadius: 8,
|
||||
minWidth: 120,
|
||||
alignItems: 'center',
|
||||
},
|
||||
retryButtonText: {
|
||||
color: theme.colors.background,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
fontFamily: theme.typography.fontFamily.medium,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -34,11 +34,24 @@ export const patientAPI = {
|
||||
* @returns Promise with medical cases data
|
||||
*/
|
||||
getPatients: (token: string) => {
|
||||
return api.get('/api/dicom/medpacks-sync/get-synced-medpacks-data', {}, buildHeaders({ token }));
|
||||
return api.get('/api/ai-cases/all-patients', {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Patient Details
|
||||
* Get Patient Details by ID
|
||||
*
|
||||
* Purpose: Fetch detailed information for a specific patient by ID
|
||||
*
|
||||
* @param patientId - Patient ID
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with patient details including predictions and series
|
||||
*/
|
||||
getPatientDetailsById: (patientId: string, token: string) => {
|
||||
return api.get(`/api/ai-cases/patient/${patientId}/predictions`, {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Patient Details (Legacy - kept for backward compatibility)
|
||||
*
|
||||
* Purpose: Fetch detailed information for a specific patient
|
||||
*
|
||||
@ -47,7 +60,7 @@ export const patientAPI = {
|
||||
* @returns Promise with patient details
|
||||
*/
|
||||
getPatientDetails: (patientId: string, token: string) => {
|
||||
return api.get(`/api/patients/${patientId}`, {}, buildHeaders({ token }));
|
||||
return api.get(`/api/ai-cases/patient/${patientId}/predictions`, {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
@ -88,9 +101,24 @@ export const patientAPI = {
|
||||
* @returns Promise with updated vital signs
|
||||
*/
|
||||
updatePatientVitals: (patientId: string, vitalSigns: any, token: string) => {
|
||||
return api.post(`/api/patients/${patientId}/vitals`, vitalSigns, buildHeaders({ token }));
|
||||
return api.put(`/api/patients/${patientId}/vitals`, vitalSigns, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit Feedback for AI Prediction
|
||||
*
|
||||
* Purpose: Submit physician feedback for AI predictions
|
||||
*
|
||||
* @param feedbackData - Feedback payload
|
||||
* @returns API response
|
||||
*/
|
||||
submitFeedback: (feedbackData: {
|
||||
patid: string;
|
||||
prediction_id: number;
|
||||
feedback_text: string;
|
||||
is_positive: boolean;
|
||||
},token) => api.post('/api/ai-cases/feedbacks', feedbackData, buildHeaders({ token })),
|
||||
|
||||
/**
|
||||
* Get Patient Medical History
|
||||
*
|
||||
|
||||
@ -6,7 +6,8 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme/theme';
|
||||
|
||||
/**
|
||||
@ -16,9 +17,13 @@ import { theme } from '../../../theme/theme';
|
||||
*
|
||||
* Props:
|
||||
* - title: Title text to display in the header
|
||||
* - showBackButton: Whether to show the back button (optional)
|
||||
* - onBackPress: Function to call when back button is pressed (optional)
|
||||
*/
|
||||
interface SettingsHeaderProps {
|
||||
title: string;
|
||||
showBackButton?: boolean;
|
||||
onBackPress?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,9 +36,22 @@ interface SettingsHeaderProps {
|
||||
* - Consistent with app theme
|
||||
* - Proper spacing and typography
|
||||
*/
|
||||
export const SettingsHeader: React.FC<SettingsHeaderProps> = ({ title }) => {
|
||||
export const SettingsHeader: React.FC<SettingsHeaderProps> = ({
|
||||
title,
|
||||
showBackButton = false,
|
||||
onBackPress
|
||||
}) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{showBackButton && onBackPress && (
|
||||
<TouchableOpacity
|
||||
style={styles.backButton}
|
||||
onPress={onBackPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Icon name="chevron-left" size={24} color={theme.colors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
</View>
|
||||
);
|
||||
@ -51,6 +69,14 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: theme.spacing.lg,
|
||||
borderBottomColor: theme.colors.border,
|
||||
borderBottomWidth: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
// Back button styling
|
||||
backButton: {
|
||||
marginRight: theme.spacing.md,
|
||||
padding: theme.spacing.xs,
|
||||
},
|
||||
|
||||
// Title text styling
|
||||
@ -58,6 +84,7 @@ const styles = StyleSheet.create({
|
||||
fontSize: theme.typography.fontSize.displayMedium,
|
||||
fontFamily: theme.typography.fontFamily.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ import { SettingsScreen } from '../screens/SettingsScreen';
|
||||
// Import navigation types
|
||||
import { SettingsStackParamList } from './navigationTypes';
|
||||
import { theme } from '../../../theme';
|
||||
import { AppInfoScreen, ChangePasswordScreen, EditProfileScreen } from '../screens';
|
||||
|
||||
// Create stack navigator for Settings module
|
||||
const Stack = createStackNavigator<SettingsStackParamList>();
|
||||
@ -78,6 +79,38 @@ const SettingsStackNavigator: React.FC = () => {
|
||||
headerShown: false, // Hide header for main settings screen
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AppInfoScreen"
|
||||
component={AppInfoScreen}
|
||||
options={{
|
||||
title: 'App Info',
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="EditProfileScreen"
|
||||
component={EditProfileScreen}
|
||||
options={{
|
||||
title: 'Edit Profile',
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ChangePasswordScreen"
|
||||
component={ChangePasswordScreen}
|
||||
options={{
|
||||
title: 'Change Password',
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
{/* <Stack.Screen
|
||||
name="HelpSupportScreen"
|
||||
component={HelpSupportScreen}
|
||||
options={{
|
||||
title: 'Help & Support',
|
||||
headerShown: false,
|
||||
}}
|
||||
/> */}
|
||||
</Stack.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
@ -19,28 +19,21 @@ export type SettingsStackParamList = {
|
||||
SettingScreen: SettingsScreenParams;
|
||||
|
||||
// Profile Edit screen - Edit user profile information
|
||||
ProfileEdit: ProfileEditScreenParams;
|
||||
// ProfileEdit: ProfileEditScreenParams;
|
||||
|
||||
// Security Settings screen - Security and privacy settings
|
||||
SecuritySettings: SecuritySettingsScreenParams;
|
||||
|
||||
// Notification Settings screen - Notification preferences
|
||||
NotificationSettings: NotificationSettingsScreenParams;
|
||||
|
||||
// Clinical Preferences screen - Clinical workflow preferences
|
||||
ClinicalPreferences: ClinicalPreferencesScreenParams;
|
||||
|
||||
// Privacy Settings screen - Privacy and data settings
|
||||
PrivacySettings: PrivacySettingsScreenParams;
|
||||
|
||||
// Accessibility Settings screen - Accessibility preferences
|
||||
AccessibilitySettings: AccessibilitySettingsScreenParams;
|
||||
|
||||
// About screen - App information and version
|
||||
About: AboutScreenParams;
|
||||
AppInfoScreen: AppInfoScreenParams;
|
||||
|
||||
// Change Password screen - Change user password
|
||||
ChangePasswordScreen: ChangePasswordScreenParams;
|
||||
|
||||
// Edit Profile screen - Edit user profile information
|
||||
EditProfileScreen: EditProfileScreenParams;
|
||||
|
||||
// Help & Support screen - Help documentation and support
|
||||
HelpSupport: HelpSupportScreenParams;
|
||||
// HelpSupport: HelpSupportScreenParams;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -238,7 +231,21 @@ export type AccessibilitySettingsScreenProps = SettingsScreenProps<'Accessibilit
|
||||
/**
|
||||
* AboutScreenProps - Props for AboutScreen component
|
||||
*/
|
||||
export type AboutScreenProps = SettingsScreenProps<'About'>;
|
||||
export type AppInfoScreenParams = SettingsScreenProps<'AppInfoScreen'>
|
||||
|
||||
/**
|
||||
* ChangePasswordScreenProps - Props for ChangePasswordScreen component
|
||||
*/
|
||||
export type ChangePasswordScreenProps = SettingsScreenProps<'ChangePasswordScreen'>;
|
||||
|
||||
|
||||
/**
|
||||
* EditProfileScreenProps - Props for EditProfileScreen component
|
||||
*/
|
||||
export type EditProfileScreenProps = SettingsScreenProps<'EditProfileScreen'>;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HelpSupportScreenProps - Props for HelpSupportScreen component
|
||||
|
||||
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,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
RefreshControl,
|
||||
Image,
|
||||
@ -35,7 +34,6 @@ import {
|
||||
selectUserFirstName,
|
||||
selectUserLastName,
|
||||
selectUserProfilePhoto,
|
||||
selectNotificationPreferences,
|
||||
selectDashboardSettings
|
||||
} from '../../Auth/redux/authSelectors';
|
||||
|
||||
@ -106,7 +104,6 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
||||
const userFirstName = useAppSelector(selectUserFirstName);
|
||||
const userLastName = useAppSelector(selectUserLastName);
|
||||
const userProfilePhoto = useAppSelector(selectUserProfilePhoto);
|
||||
const notificationPreferences = useAppSelector(selectNotificationPreferences);
|
||||
const dashboardSettings = useAppSelector(selectDashboardSettings);
|
||||
|
||||
|
||||
@ -142,79 +139,10 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
||||
type: 'NAVIGATION',
|
||||
onPress: () => handleNavigation('CHANGE_PASSWORD'),
|
||||
},
|
||||
{
|
||||
id: 'security-settings',
|
||||
title: 'Security Settings',
|
||||
subtitle: 'Two-factor authentication and biometrics',
|
||||
icon: 'shield',
|
||||
type: 'NAVIGATION',
|
||||
onPress: () => handleNavigation('SECURITY'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'NOTIFICATIONS',
|
||||
title: 'Notifications',
|
||||
items: [
|
||||
{
|
||||
id: 'notification-preferences',
|
||||
title: 'Notification Preferences',
|
||||
subtitle: 'Manage alert and notification settings',
|
||||
icon: 'bell',
|
||||
type: 'NAVIGATION',
|
||||
onPress: () => handleNavigation('NOTIFICATIONS'),
|
||||
},
|
||||
{
|
||||
id: 'quiet-hours',
|
||||
title: 'Quiet Hours',
|
||||
subtitle: 'Set do not disturb periods',
|
||||
icon: 'moon',
|
||||
type: 'NAVIGATION',
|
||||
onPress: () => handleNavigation('QUIET_HOURS'),
|
||||
},
|
||||
{
|
||||
id: 'push-notifications',
|
||||
title: 'Push Notifications',
|
||||
subtitle: 'Enable or disable push notifications',
|
||||
icon: 'phone',
|
||||
type: 'TOGGLE',
|
||||
value: notificationPreferences?.system_notifications.push,
|
||||
onPress: () => handleToggleSetting('pushNotifications'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
id: 'PRIVACY',
|
||||
title: 'Privacy & Security',
|
||||
items: [
|
||||
{
|
||||
id: 'privacy-settings',
|
||||
title: 'Privacy Settings',
|
||||
subtitle: 'Manage data sharing and privacy controls',
|
||||
icon: 'settings',
|
||||
type: 'NAVIGATION',
|
||||
onPress: () => handleNavigation('PRIVACY'),
|
||||
},
|
||||
{
|
||||
id: 'biometric-auth',
|
||||
title: 'Biometric Authentication',
|
||||
subtitle: 'Use fingerprint or face ID',
|
||||
icon: 'lock',
|
||||
type: 'TOGGLE',
|
||||
value: false, // TODO: Add biometric auth preference to user data
|
||||
onPress: () => handleToggleSetting('biometricAuth'),
|
||||
},
|
||||
{
|
||||
id: 'session-timeout',
|
||||
title: 'Session Timeout',
|
||||
subtitle: 'Auto-logout after inactivity',
|
||||
icon: 'clock',
|
||||
type: 'NAVIGATION',
|
||||
onPress: () => handleNavigation('SESSION_TIMEOUT'),
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
id: 'ABOUT',
|
||||
@ -236,14 +164,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
||||
type: 'NAVIGATION',
|
||||
onPress: () => handleNavigation('HELP'),
|
||||
},
|
||||
{
|
||||
id: 'feedback',
|
||||
title: 'Send Feedback',
|
||||
subtitle: 'Report bugs or suggest improvements',
|
||||
icon: 'rss',
|
||||
type: 'NAVIGATION',
|
||||
onPress: () => handleNavigation('FEEDBACK'),
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -273,7 +194,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
||||
*/
|
||||
useEffect(() => {
|
||||
setSettingsSections(generateSettingsSections());
|
||||
}, [user, notificationPreferences, dashboardSettings]);
|
||||
}, [user, dashboardSettings]);
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
@ -310,17 +231,40 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
||||
* @param screen - Screen to navigate to
|
||||
*/
|
||||
const handleNavigation = (screen: string) => {
|
||||
// TODO: Implement navigation to specific settings screens
|
||||
console.log('Navigate to:', screen);
|
||||
setModalConfig({
|
||||
title: 'Navigation',
|
||||
message: `Navigate to ${screen} screen`,
|
||||
type: 'info',
|
||||
onConfirm: () => {},
|
||||
showCancel: false,
|
||||
icon: 'info',
|
||||
});
|
||||
setModalVisible(true);
|
||||
switch (screen) {
|
||||
case 'APP_INFO':
|
||||
navigation.navigate('AppInfoScreen');
|
||||
break;
|
||||
case 'PROFILE':
|
||||
navigation.navigate('EditProfileScreen');
|
||||
break;
|
||||
case 'CHANGE_PASSWORD':
|
||||
navigation.navigate('ChangePasswordScreen');
|
||||
break;
|
||||
case 'HELP':
|
||||
// TODO: Implement help and support
|
||||
setModalConfig({
|
||||
title: 'Help & Support',
|
||||
message: 'Help and support functionality coming soon!',
|
||||
type: 'info',
|
||||
onConfirm: () => {},
|
||||
showCancel: false,
|
||||
icon: 'info',
|
||||
});
|
||||
setModalVisible(true);
|
||||
break;
|
||||
default:
|
||||
console.log('Navigate to:', screen);
|
||||
setModalConfig({
|
||||
title: 'Navigation',
|
||||
message: `Navigate to ${screen} screen`,
|
||||
type: 'info',
|
||||
onConfirm: () => {},
|
||||
showCancel: false,
|
||||
icon: 'info',
|
||||
});
|
||||
setModalVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -387,14 +331,7 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* handleProfilePress Function
|
||||
*
|
||||
* Purpose: Handle profile card press navigation
|
||||
*/
|
||||
const handleProfilePress = () => {
|
||||
handleNavigation('PROFILE');
|
||||
};
|
||||
|
||||
|
||||
// ============================================================================
|
||||
// MAIN RENDER
|
||||
@ -422,37 +359,31 @@ export const SettingsScreen: React.FC<SettingsScreenProps> = ({
|
||||
{/* Profile card section */}
|
||||
{user && (
|
||||
<View style={styles.profileCard}>
|
||||
<TouchableOpacity onPress={handleProfilePress} activeOpacity={0.7}>
|
||||
<View style={styles.profileHeader}>
|
||||
<View style={styles.profileImageContainer}>
|
||||
{user.profile_photo_url ? (
|
||||
<Image
|
||||
source={{ uri: user.profile_photo_url }}
|
||||
style={styles.profileImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.fallbackAvatar}>
|
||||
<Text style={styles.fallbackText}>
|
||||
{user.first_name.charAt(0)}{user.last_name.charAt(0)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.profileInfo}>
|
||||
<Text style={styles.profileName}>
|
||||
{user.display_name || `${user.first_name} ${user.last_name}`}
|
||||
</Text>
|
||||
<Text style={styles.profileEmail}>{user.email}</Text>
|
||||
<Text style={styles.profileRole}>Physician</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.editIcon}>
|
||||
<Text style={styles.editText}>Edit</Text>
|
||||
</View>
|
||||
<View style={styles.profileHeader}>
|
||||
<View style={styles.profileImageContainer}>
|
||||
{user.profile_photo_url ? (
|
||||
<Image
|
||||
source={{ uri: user.profile_photo_url }}
|
||||
style={styles.profileImage}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.fallbackAvatar}>
|
||||
<Text style={styles.fallbackText}>
|
||||
{user.first_name.charAt(0)}{user.last_name.charAt(0)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
<View style={styles.profileInfo}>
|
||||
<Text style={styles.profileName}>
|
||||
{user.display_name || `${user.first_name} ${user.last_name}`}
|
||||
</Text>
|
||||
<Text style={styles.profileEmail}>{user.email}</Text>
|
||||
<Text style={styles.profileRole}>Radiologist</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@ -589,18 +520,7 @@ const styles = StyleSheet.create({
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
|
||||
editIcon: {
|
||||
paddingHorizontal: theme.spacing.sm,
|
||||
paddingVertical: theme.spacing.xs,
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
borderRadius: theme.borderRadius.small,
|
||||
},
|
||||
|
||||
editText: {
|
||||
fontSize: theme.typography.fontSize.bodySmall,
|
||||
fontFamily: theme.typography.fontFamily.medium,
|
||||
color: theme.colors.primary,
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
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 */}
|
||||
<Tab.Screen
|
||||
{/* <Tab.Screen
|
||||
name="AIPredictions"
|
||||
component={AIPredictionStackNavigator}
|
||||
options={{
|
||||
@ -110,7 +110,7 @@ export const MainTabNavigator: React.FC = () => {
|
||||
),
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{/* Reports Tab - Medical documentation */}
|
||||
{/* <Tab.Screen
|
||||
|
||||
@ -96,8 +96,22 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal
|
||||
const timer = setTimeout(() => {
|
||||
if (webViewRef.current) {
|
||||
try {
|
||||
// Send the URL directly as a string message
|
||||
webViewRef.current.postMessage(dicomUrl);
|
||||
debugLog('DICOM URL sent successfully');
|
||||
|
||||
// Also try sending as a structured message
|
||||
setTimeout(() => {
|
||||
if (webViewRef.current) {
|
||||
const structuredMessage = JSON.stringify({
|
||||
type: 'loadDicom',
|
||||
data: dicomUrl
|
||||
});
|
||||
webViewRef.current.postMessage(structuredMessage);
|
||||
debugLog('Structured DICOM message sent');
|
||||
}
|
||||
}, 500);
|
||||
|
||||
} catch (error) {
|
||||
debugLog(`Failed to send DICOM URL: ${error}`);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user