Loading DICOM Viewer...
-
Debug Info:
-
+
+
+
🩻
+
DICOM Viewer
+
No image loaded
DICOM files will be loaded from parent component
+
-
+
+
+
- // Debug logging function
- function debugLog(message, type = 'info') {
- if (debugMode) {
- console.log(`[DICOM Viewer] ${message}`);
- const debugContent = document.getElementById('debugContent');
- if (debugContent) {
- const timestamp = new Date().toLocaleTimeString();
- debugContent.innerHTML += `
[${timestamp}] ${message}
`;
- }
- }
- }
+
+
+ // Show error in the viewer
+ element.innerHTML = `
+
+
❌
+
Error Loading DICOM
+
${err.message}
URL: ${imageId}
+
+ `;
+
+ showError('Error loading DICOM: ' + err.message);
+ });
+}
+
+function loadSeries(imageIds) {
+ const stack = { currentImageIdIndex: 0, imageIds };
+ cornerstone.loadImage(imageIds[0]).then(image => {
+ currentImage = image;
+ cornerstone.displayImage(element, image);
+ setupStack(stack);
+ showSuccess(`Series loaded: ${imageIds.length} images`);
+ fitToWindow();
+ }).catch(err => {
+ showError('Error loading series: ' + err.message);
+ });
+}
+
+function setupStack(stack) {
+ currentStack = stack;
+ cornerstoneTools.addStackStateManager(element, ['stack']);
+ cornerstoneTools.addToolState(element, 'stack', stack);
+ updateFrameInfo();
+
+ element.addEventListener('cornerstonetoolsstackscroll', () => {
+ updateFrameInfo();
+ });
+}
+
+function updateFrameInfo() {
+ if (currentStack) {
+ frameInfo.textContent = `${currentStack.currentImageIdIndex + 1} / ${currentStack.imageIds.length}`;
+ prevFrameBtn.disabled = currentStack.currentImageIdIndex === 0;
+ nextFrameBtn.disabled = currentStack.currentImageIdIndex === currentStack.imageIds.length - 1;
+ } else {
+ frameInfo.textContent = 'No images';
+ prevFrameBtn.disabled = true;
+ nextFrameBtn.disabled = true;
+ }
+}
+
+function previousFrame() {
+ if (currentStack && currentStack.currentImageIdIndex > 0) {
+ currentStack.currentImageIdIndex--;
+ cornerstone.loadImage(currentStack.imageIds[currentStack.currentImageIdIndex]).then(image => {
+ cornerstone.displayImage(element, image);
+ updateFrameInfo();
+ });
+ }
+}
+
+function nextFrame() {
+ if (currentStack && currentStack.currentImageIdIndex < currentStack.imageIds.length - 1) {
+ currentStack.currentImageIdIndex++;
+ cornerstone.loadImage(currentStack.imageIds[currentStack.currentImageIdIndex]).then(image => {
+ cornerstone.displayImage(element, image);
+ updateFrameInfo();
+ });
+ }
+}
+
+function zoomIn() {
+ const viewport = cornerstone.getViewport(element);
+ viewport.scale *= 1.5;
+ cornerstone.setViewport(element, viewport);
+}
+
+function zoomOut() {
+ const viewport = cornerstone.getViewport(element);
+ viewport.scale /= 1.5;
+ cornerstone.setViewport(element, viewport);
+}
+
+function fitToWindow() {
+ if (currentImage) {
+ cornerstone.fitToWindow(element);
+ }
+}
+
+function resetView() {
+ if (currentImage) {
+ cornerstone.reset(element);
+ fitToWindow();
+ }
+}
+
+function clearView() {
+ cornerstone.disable(element);
+ cornerstone.enable(element);
+ currentStack = null;
+ currentImage = null;
+ updateFrameInfo();
+ statusText.textContent = 'Ready to load DICOM files';
+ statusText.style.color = 'white';
+
+ // Show preview again
+ element.innerHTML = `
+
+
🩻
+
DICOM Viewer
+
No image loaded
DICOM files will be loaded from parent component
+
+ `;
+
+ // Reactivate tools
+ cornerstoneTools.setToolActive('PanMultiTouch', {});
+ cornerstoneTools.setToolActive('ZoomTouchPinch', {});
+ cornerstoneTools.setToolActive('StackScrollMultiTouch', {});
+ cornerstoneTools.setToolActive('WwwcRegion', {});
+}
+
+// Initialize
+updateFrameInfo();
+
+// Expose functions for parent component
+window.DicomViewer = {
+ loadDicom: loadDicomFromParent,
+ loadSeries: loadSeriesFromParent,
+ resetView: resetView,
+ clearView: clearView,
+ zoomIn: zoomIn,
+ zoomOut: zoomOut,
+ fitToWindow: fitToWindow,
+ previousFrame: previousFrame,
+ nextFrame: nextFrame
+};
+
diff --git a/app/modules/AIPrediction/navigation/AIPredictionStackNavigator.tsx b/app/modules/AIPrediction/navigation/AIPredictionStackNavigator.tsx
index 6cf719d..c5c070f 100644
--- a/app/modules/AIPrediction/navigation/AIPredictionStackNavigator.tsx
+++ b/app/modules/AIPrediction/navigation/AIPredictionStackNavigator.tsx
@@ -152,7 +152,12 @@ const AIPredictionStackNavigator: React.FC = () => {
{/* AI Prediction Details Screen */}
}
+ component={() => console.log('DICOM Error:', error)}
+ onLoad={() => console.log('DICOM Viewer loaded successfully')}
+ />}
options={({ navigation, route }) => ({
title: 'Create Suggestion',
headerLeft: () => (
diff --git a/app/modules/Auth/redux/authActions.ts b/app/modules/Auth/redux/authActions.ts
index d37ed74..977d6be 100644
--- a/app/modules/Auth/redux/authActions.ts
+++ b/app/modules/Auth/redux/authActions.ts
@@ -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
diff --git a/app/modules/Auth/screens/LoginScreen.tsx b/app/modules/Auth/screens/LoginScreen.tsx
index 30b7495..259b9a5 100644
--- a/app/modules/Auth/screens/LoginScreen.tsx
+++ b/app/modules/Auth/screens/LoginScreen.tsx
@@ -138,8 +138,8 @@ const LoginScreen: React.FC = ({ navigation }) => {
* HEADER SECTION - App branding and title
* ======================================================================== */}
- Physician
- Emergency Department Access
+ Radiologist
+ {/* Emergency Department Access */}
diff --git a/app/modules/Auth/screens/SignUpScreen.tsx b/app/modules/Auth/screens/SignUpScreen.tsx
index 12e6416..adfcd74 100644
--- a/app/modules/Auth/screens/SignUpScreen.tsx
+++ b/app/modules/Auth/screens/SignUpScreen.tsx
@@ -226,7 +226,7 @@ const SignUpScreen: React.FC = ({ navigation }) => {
setIsLoading(true);
try {
- let role = 'er_physician';
+ let role = 'radiologist';
// Prepare form data with proper file handling
const formFields = {
diff --git a/app/modules/Auth/services/authAPI.ts b/app/modules/Auth/services/authAPI.ts
index 784bfda..156e4cc 100644
--- a/app/modules/Auth/services/authAPI.ts
+++ b/app/modules/Auth/services/authAPI.ts
@@ -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
};
diff --git a/app/modules/Dashboard/components/DashboardHeader.tsx b/app/modules/Dashboard/components/DashboardHeader.tsx
index 06ae15c..5606b63 100644
--- a/app/modules/Dashboard/components/DashboardHeader.tsx
+++ b/app/modules/Dashboard/components/DashboardHeader.tsx
@@ -29,33 +29,6 @@ export const DashboardHeader: React.FC = ({
{dashboard.shiftInfo.currentShift} Shift • {dashboard.shiftInfo.attendingPhysician}
-
-
-
- {dashboard.totalPatients}
- Total Patients
-
-
-
- {dashboard.criticalPatients}
-
- Critical
-
-
- {dashboard.pendingScans}
- Pending Scans
-
-
- {dashboard.bedOccupancy}%
- Bed Occupancy
-
-
-
-
-
- Last updated: {dashboard.lastUpdated.toLocaleTimeString()}
-
-
);
};
@@ -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
diff --git a/app/modules/Dashboard/index.ts b/app/modules/Dashboard/index.ts
index 53cb34d..a326b4d 100644
--- a/app/modules/Dashboard/index.ts
+++ b/app/modules/Dashboard/index.ts
@@ -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,
diff --git a/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx b/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx
index f430b35..ee262f1 100644
--- a/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx
+++ b/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx
@@ -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();
* 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 */}
{
// ============================================================================
/**
- * ERDashboardScreenParams
+ * DashboardScreenParams
*
* Purpose: Parameters passed to the ER dashboard screen
*
@@ -67,7 +67,7 @@ export interface DashboardScreenProps {
* - 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
diff --git a/app/modules/Dashboard/screens/DashboardScreen.tsx b/app/modules/Dashboard/screens/DashboardScreen.tsx
new file mode 100644
index 0000000..71b3be5
--- /dev/null
+++ b/app/modules/Dashboard/screens/DashboardScreen.tsx
@@ -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;
+ critical_findings: Record;
+ midline_shift_stats: Record;
+ hemorrhage_stats: Record;
+ mass_lesion_stats: Record;
+ edema_stats: Record;
+ fracture_stats: Record;
+ feedback_analysis: {
+ positive: number;
+ negative: number;
+ total: number;
+ };
+ hospital_distribution: Record;
+ 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 = ({
+ navigation,
+}) => {
+ // ============================================================================
+ // STATE MANAGEMENT
+ // ============================================================================
+
+ // Refresh state for pull-to-refresh functionality
+ const [refreshing, setRefreshing] = useState(false);
+
+ // Dashboard data state
+ const [dashboardData, setDashboardData] = useState(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) => (
+
+ {title}
+ {value}
+ {subtitle && {subtitle}}
+
+ );
+
+ /**
+ * 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 (
+
+ Confidence Score Distribution
+
+
+
+ High
+ {high}
+
+
+
+ Medium
+ {medium}
+
+
+
+ Low
+ {low}
+
+
+
+ );
+ };
+
+ /**
+ * 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 (
+
+ Case Urgency Distribution
+
+
+
+ Critical
+ {critical}
+
+
+
+ Urgent
+ {urgent}
+
+
+
+ Routine
+ {routine}
+
+
+
+ );
+ };
+
+ /**
+ * 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 (
+
+ Feedback Analysis
+
+
+
+ Positive
+ {positive}
+ ({positivePercentage}%)
+
+
+
+ Negative
+ {negative}
+ ({negativePercentage}%)
+
+
+
+
+ Feedback Coverage: {dashboardData.data.feedback_rate_percentage}%
+
+
+ Average Feedback per Prediction: {dashboardData.data.average_feedback_per_prediction}
+
+
+
+ );
+ };
+
+ /**
+ * 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 (
+
+ Time-based Analysis
+
+
+ Today
+ {today}
+
+
+ This Week
+ {this_week}
+
+
+ This Month
+ {this_month}
+
+
+ This Year
+ {this_year}
+
+
+
+ );
+ };
+
+ /**
+ * renderHeader Function
+ *
+ * Purpose: Render the dashboard header section with key metrics
+ */
+ const renderHeader = () => (
+
+ {/* Dashboard header with title and refresh button */}
+
+ AI Analysis Dashboard
+
+ {dashboardData?.message || 'Loading statistics...'}
+
+
+
+ {/* Key statistics cards */}
+
+ {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
+ )}
+
+
+ );
+
+ // ============================================================================
+ // LOADING STATE
+ // ============================================================================
+
+ /**
+ * Loading state render
+ *
+ * Purpose: Show loading indicator while data is being generated
+ */
+ if (isLoading) {
+ return (
+
+ Loading AI Analysis Dashboard...
+
+ );
+ }
+
+ // ============================================================================
+ // MAIN RENDER
+ // ============================================================================
+
+ return (
+
+ {/* Scrollable dashboard content */}
+
+ }
+ 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 */}
+
+
+
+ );
+};
+
+// ============================================================================
+// 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.
+ */
\ No newline at end of file
diff --git a/app/modules/Dashboard/screens/ERDashboardScreen.tsx b/app/modules/Dashboard/screens/ERDashboardScreen.tsx
index be7c4d4..3dd844c 100644
--- a/app/modules/Dashboard/screens/ERDashboardScreen.tsx
+++ b/app/modules/Dashboard/screens/ERDashboardScreen.tsx
@@ -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 = ({
/>
)}
- {/* Quick action buttons for brain imaging tasks */}
- {
- console.log('Quick action:', action);
- }}
- />
-
{/* Department statistics showing brain case distribution */}
{dashboard && }
diff --git a/app/modules/Dashboard/screens/patient.json b/app/modules/Dashboard/screens/patient.json
new file mode 100644
index 0000000..2607766
--- /dev/null
+++ b/app/modules/Dashboard/screens/patient.json
@@ -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"
+}
\ No newline at end of file
diff --git a/app/modules/PatientCare/components/FilterTabs.tsx b/app/modules/PatientCare/components/FilterTabs.tsx
index 0d35f6f..825c623 100644
--- a/app/modules/PatientCare/components/FilterTabs.tsx
+++ b/app/modules/PatientCare/components/FilterTabs.tsx
@@ -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 = ({
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 = ({
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 = ({
- {/* Critical Indicator */}
- {tab.id === 'Critical' && patientCount > 0 && (
-
+ {/* Error Indicator */}
+ {tab.id === 'error' && patientCount > 0 && (
+
)}
@@ -300,8 +300,8 @@ const styles = StyleSheet.create({
color: theme.colors.background,
},
- // Critical Indicator
- criticalIndicator: {
+ // Error Indicator
+ errorIndicator: {
position: 'absolute',
top: 8,
right: 8,
diff --git a/app/modules/PatientCare/components/PatientCard.tsx b/app/modules/PatientCare/components/PatientCard.tsx
index 983f4c1..889120b 100644
--- a/app/modules/PatientCare/components/PatientCard.tsx
+++ b/app/modules/PatientCare/components/PatientCard.tsx
@@ -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 = ({
// ============================================================================
/**
- * 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 = ({
* @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 = ({
});
};
+ /**
+ * 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 = () => (
-
-
- {patient.type}
+ const renderStatusBadge = () => (
+
+
+ {patientInfo.status}
);
@@ -183,7 +181,7 @@ const PatientCard: React.FC = ({
* 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 = ({
= ({
- {patientData.Name || 'Unknown Patient'}
+ {patientInfo.name || 'Unknown Patient'}
- 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'}
- {renderTypeBadge()}
+ {renderStatusBadge()}
{renderEmergencyButton()}
@@ -237,27 +235,27 @@ const PatientCard: React.FC = ({
- {patientData.Modality || 'N/A'}
+ {patientInfo.modality || 'N/A'}
- Status
+ Files
- {patientData.Status || 'Unknown'}
+ {patient.total_files_processed}
Report
- {patientData.ReportStatus || 'Pending'}
+ {patientInfo.report_status || 'Pending'}
@@ -266,7 +264,7 @@ const PatientCard: React.FC = ({
- {patientData.InstName || 'Unknown Institution'}
+ {patientInfo.institution || 'Unknown Institution'}
@@ -278,17 +276,22 @@ const PatientCard: React.FC = ({
Series Information
- {Array.isArray(series) ? series.length : 0} Series Available
+ {seriesCount} Series Available • {patientInfo.frame_count} Total Frames
{/* Footer */}
-
- {formatDate(patient.created_at)}
-
+
+
+ {formatDate(patientInfo.date)}
+
+
+ {getTimeSinceProcessed(patient.last_processed_at)}
+
+
- Case #{patient.id}
+ Case #{patient.patid}
@@ -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',
diff --git a/app/modules/PatientCare/navigation/navigationTypes.ts b/app/modules/PatientCare/navigation/navigationTypes.ts
index aa46fd3..45588f6 100644
--- a/app/modules/PatientCare/navigation/navigationTypes.ts
+++ b/app/modules/PatientCare/navigation/navigationTypes.ts
@@ -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;
}
// ============================================================================
diff --git a/app/modules/PatientCare/redux/patientCareSelectors.ts b/app/modules/PatientCare/redux/patientCareSelectors.ts
index efdae2c..4ecc20b 100644
--- a/app/modules/PatientCare/redux/patientCareSelectors.ts
+++ b/app/modules/PatientCare/redux/patientCareSelectors.ts
@@ -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,
+ };
+ }
);
/*
diff --git a/app/modules/PatientCare/redux/patientCareSlice.ts b/app/modules/PatientCare/redux/patientCareSlice.ts
index aacc24d..8222b65 100644
--- a/app/modules/PatientCare/redux/patientCareSlice.ts
+++ b/app/modules/PatientCare/redux/patientCareSlice.ts
@@ -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 & { id: number }, { rejectWithValue }) => {
+ async (patientData: Partial & { 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) => {
+ setCurrentPatient: (state, action: PayloadAction) => {
state.currentPatient = action.payload;
},
@@ -323,14 +386,14 @@ const patientCareSlice = createSlice({
*
* Purpose: Update a patient in the patients list
*/
- updatePatientInList: (state, action: PayloadAction) => {
- const index = state.patients.findIndex(patient => patient.id === action.payload.id);
+ updatePatientInList: (state, action: PayloadAction) => {
+ 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) => {
+ addPatient: (state, action: PayloadAction) => {
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) => {
- const index = state.patients.findIndex(patient => patient.id === action.payload);
+ removePatient: (state, action: PayloadAction) => {
+ 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 };
}
})
diff --git a/app/modules/PatientCare/screens/PatientDetailsScreen.tsx b/app/modules/PatientCare/screens/PatientDetailsScreen.tsx
index d790898..548eea0 100644
--- a/app/modules/PatientCare/screens/PatientDetailsScreen.tsx
+++ b/app/modules/PatientCare/screens/PatientDetailsScreen.tsx
@@ -1,8 +1,16 @@
/*
* File: PatientDetailsScreen.tsx
- * Description: Comprehensive patient details screen with DICOM image viewer
+ * Description: Comprehensive patient details screen with DICOM image viewer and AI analysis
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
+ *
+ * Features:
+ * - Patient demographics and medical information
+ * - Merged AI Analysis tab showing DICOM images alongside AI predictions
+ * - Processing history and timeline
+ * - Responsive design for different screen sizes
+ * - Emergency actions for critical cases
+ * - Clinical feedback system for AI predictions and DICOM images
*/
import React, { useEffect, useState, useCallback, useMemo } from 'react';
@@ -18,17 +26,16 @@ import {
Image,
FlatList,
RefreshControl,
+ TextInput,
} from 'react-native';
import { theme } from '../../../theme/theme';
-import { useAppDispatch } from '../../../store/hooks';
+import { useAppDispatch, useAppSelector } from '../../../store/hooks';
import Icon from 'react-native-vector-icons/Feather';
import { SafeAreaView } from 'react-native-safe-area-context';
-// Import types
-import { MedicalCase, PatientDetails, Series } from '../../../shared/types';
-
-// Import components
-import { ImageViewer } from '../components';
+// Import types and API
+import { patientAPI } from '../services/patientAPI';
+import { selectUser } from '../../Auth/redux/authSelectors';
import { API_CONFIG } from '../../../shared/utils';
// Get screen dimensions
@@ -43,15 +50,60 @@ interface PatientDetailsScreenProps {
route: {
params: {
patientId: string;
- patientName: string;
- medicalCase: MedicalCase;
+ patientName?: string;
};
};
}
-interface ParsedPatientData {
- patientDetails: PatientDetails;
- series: Series[];
+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;
+}
+
+interface SeriesSummary {
+ series_num: string;
+ series_description: string;
+ total_images: number;
+ png_preview: string;
+ modality: string;
+}
+
+interface Prediction {
+ id: number;
+ file_path: string;
+ prediction: {
+ label: string;
+ finding_type: string;
+ clinical_urgency: string;
+ confidence_score: number;
+ detailed_results: any;
+ finding_category: string;
+ primary_severity: string;
+ anatomical_location: string;
+ };
+ processed_at: string;
+ preview: string;
+}
+
+interface PatientData {
+ patid: string;
+ hospital_id: string;
+ patient_info: PatientInfo;
+ series_summary: SeriesSummary[];
+ processing_metadata: any;
+ total_predictions: number;
+ first_processed_at: string;
+ last_processed_at: string;
+ predictions_by_series: { [key: string]: Prediction[] };
}
// ============================================================================
@@ -67,8 +119,9 @@ interface ParsedPatientData {
* - Full patient demographic information
* - Medical case details and status
* - DICOM series information
+ * - AI predictions and findings
* - Image gallery with thumbnail previews
- * - Real-time data updates
+ * - Real-time data updates from API
* - Emergency actions for critical cases
* - Medical history and notes
* - Modern healthcare-focused UI design
@@ -82,64 +135,96 @@ const PatientDetailsScreen: React.FC = ({ navigation,
const dispatch = useAppDispatch();
// Route parameters
- const { patientId, patientName, medicalCase } = route.params;
+ const { patientId, patientName } = route.params;
+
+ // Redux state
+ const user = useAppSelector(selectUser);
// Local state
+ const [patientData, setPatientData] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
+ const [error, setError] = useState(null);
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const [showFullImage, setShowFullImage] = useState(false);
- const [activeTab, setActiveTab] = useState<'overview' | 'images' | 'history'>('overview');
+ const [activeTab, setActiveTab] = useState<'overview' | 'aiAnalysis' | 'history'>('overview');
+
+ // Feedback state
+ const [showFeedbackModal, setShowFeedbackModal] = useState(false);
+ const [selectedSeriesForFeedback, setSelectedSeriesForFeedback] = useState(null);
+ const [selectedPrediction, setSelectedPrediction] = useState(null);
+ const [feedbackText, setFeedbackText] = useState('');
+ const [isPositive, setIsPositive] = useState(null);
+ const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false);
+
+ // Feedback result modal state
+ const [showFeedbackResultModal, setShowFeedbackResultModal] = useState(false);
+ const [feedbackResult, setFeedbackResult] = useState<{
+ type: 'success' | 'error';
+ title: string;
+ message: string;
+ } | null>(null);
// ============================================================================
- // DATA PARSING & PROCESSING
+ // DATA FETCHING
// ============================================================================
/**
- * Parse Patient Data
+ * Fetch Patient Data
*
- * Purpose: Safely parse JSON strings from medical case data
+ * Purpose: Fetch patient details from API
*/
- const parsedData: ParsedPatientData = useMemo(() => {
- 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 fetchPatientData = useCallback(async () => {
+ if (!user?.access_token) {
+ setError('Authentication token not available');
+ setIsLoading(false);
+ return;
+ }
- const patientDetails = parseJsonSafely(medicalCase.patientdetails);
- const series = parseJsonSafely(patientDetails.series);
-
- return {
- patientDetails: patientDetails.patientdetails || patientDetails,
- series: Array.isArray(series) ? series : [],
- };
- }, [medicalCase]);
+ try {
+ setIsLoading(true);
+ setError(null);
+
+ const response: any = await patientAPI.getPatientDetailsById(patientId, user.access_token);
+
+ if (response.ok && response.data && response.data.data ) {
+ setPatientData(response.data.data as PatientData);
+ } else {
+ setError(response.problem || 'Failed to fetch patient data');
+ }
+ } catch (err: any) {
+ setError(err.message || 'An error occurred while fetching patient data');
+ } finally {
+ setIsLoading(false);
+ }
+ }, [patientId, user?.access_token]);
// ============================================================================
- // LIFECYCLE METHODS
+ // EFFECTS
// ============================================================================
/**
* Component Mount Effect
*
- * Purpose: Initialize screen and set up navigation
+ * Purpose: Initialize screen and fetch patient data
*/
useEffect(() => {
- // Set navigation title
- navigation.setOptions({
- title: patientName || 'Patient Details',
- headerShown: false,
- });
- }, [navigation, patientName]);
+ fetchPatientData();
+ }, [fetchPatientData]);
+
+ /**
+ * Navigation Title Effect
+ *
+ * Purpose: Set navigation title when patient data is loaded
+ */
+ useEffect(() => {
+ if (patientData) {
+ navigation.setOptions({
+ title: patientData.patient_info.name || 'Patient Details',
+ headerShown: false,
+ });
+ }
+ }, [navigation, patientData]);
// ============================================================================
// EVENT HANDLERS
@@ -152,11 +237,9 @@ const PatientDetailsScreen: React.FC = ({ navigation,
*/
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
- // TODO: Implement refresh logic
- setTimeout(() => {
- setIsRefreshing(false);
- }, 1000);
- }, []);
+ await fetchPatientData();
+ setIsRefreshing(false);
+ }, [fetchPatientData]);
/**
* Handle Image Press
@@ -182,18 +265,19 @@ const PatientDetailsScreen: React.FC = ({ navigation,
/**
* Get All Images from Series
*
- * Purpose: Extract image paths from DICOM series pngpath
+ * Purpose: Extract image paths from DICOM series
*/
const getAllImages = useCallback(() => {
+ if (!patientData) return [];
+
const images: string[] = [];
- parsedData.series.forEach(series => {
- // Use pngpath for actual image display
- if (series.pngpath && typeof series.pngpath === 'string') {
- images.push(series.pngpath);
+ patientData.series_summary.forEach(series => {
+ if (series.png_preview) {
+ images.push(series.png_preview);
}
});
return images;
- }, [parsedData.series]);
+ }, [patientData]);
/**
* Get Series Info for Image Index
@@ -201,22 +285,23 @@ const PatientDetailsScreen: React.FC = ({ navigation,
* Purpose: Get series information for a given image index
*/
const getSeriesInfoForImage = useCallback((imageIndex: number) => {
- if (imageIndex >= 0 && imageIndex < parsedData.series.length) {
- const series = parsedData.series[imageIndex];
+ if (!patientData || imageIndex < 0 || imageIndex >= patientData.series_summary.length) {
return {
- seriesNum: series.SeriesNum || '1',
- seriesDesc: series.SerDes || 'Unnamed Series',
+ seriesNum: '1',
+ seriesDesc: 'Unknown Series',
imageInSeries: 1,
totalInSeries: 1
};
}
+
+ const series = patientData.series_summary[imageIndex];
return {
- seriesNum: '1',
- seriesDesc: 'Unknown Series',
+ seriesNum: series.series_num,
+ seriesDesc: series.series_description,
imageInSeries: 1,
- totalInSeries: 1
+ totalInSeries: series.total_images
};
- }, [parsedData.series]);
+ }, [patientData]);
/**
* Handle Emergency Action
@@ -224,9 +309,11 @@ const PatientDetailsScreen: React.FC = ({ navigation,
* Purpose: Handle emergency actions for critical patients
*/
const handleEmergencyAction = useCallback(() => {
+ if (!patientData) return;
+
Alert.alert(
'Emergency Action Required',
- `Patient ${parsedData.patientDetails.Name} requires immediate attention`,
+ `Patient ${patientData.patient_info.name} requires immediate attention`,
[
{
text: 'Call Code Blue',
@@ -249,7 +336,7 @@ const PatientDetailsScreen: React.FC = ({ navigation,
},
]
);
- }, [parsedData.patientDetails.Name]);
+ }, [patientData]);
/**
* Handle Back Navigation
@@ -261,309 +348,119 @@ const PatientDetailsScreen: React.FC = ({ navigation,
}, [navigation]);
// ============================================================================
- // RENDER HELPERS
+ // FEEDBACK HANDLERS
// ============================================================================
/**
- * Render Patient Header
+ * Handle Open Feedback Modal
*
- * Purpose: Render patient identification and status section
+ * Purpose: Open feedback modal for a specific series and prediction
+ *
+ * @param series - Series data for feedback
+ * @param prediction - Prediction data for feedback
*/
- const renderPatientHeader = () => (
-
-
-
-
-
-
-
- {parsedData.patientDetails.Name || 'Unknown Patient'}
-
-
- MRN: {parsedData.patientDetails.PatID || 'N/A'}
-
-
-
- {parsedData.patientDetails.PatAge || 'N/A'} • {parsedData.patientDetails.PatSex || 'N/A'}
-
-
- {medicalCase.type}
-
-
-
-
+ const handleOpenFeedback = useCallback((series: SeriesSummary, prediction: Prediction) => {
+ setSelectedSeriesForFeedback(series);
+ setSelectedPrediction(prediction);
+ setFeedbackText('');
+ setIsPositive(null);
+ setShowFeedbackModal(true);
+ }, []);
+
+ /**
+ * Handle Submit Feedback
+ *
+ * Purpose: Submit feedback to API
+ */
+ const handleSubmitFeedback = useCallback(async () => {
+ if ( !selectedPrediction || !feedbackText.trim() || isPositive === null) {
+ setFeedbackResult({
+ type: 'error',
+ title: 'Validation Error',
+ message: 'Please provide all required feedback information'
+ });
+ setShowFeedbackResultModal(true);
+ return;
+ }
+
+ try {
+ setIsSubmittingFeedback(true);
- {medicalCase.type === 'Critical' && (
-
-
- EMERGENCY
-
- )}
-
- );
+ if (!patientData?.patid) {
+ throw new Error('Patient ID not available');
+ }
+
+ const feedbackPayload = {
+ patid: patientData.patid,
+ prediction_id: selectedPrediction.id,
+ feedback_text: feedbackText.trim(),
+ is_positive: isPositive
+ };
+
+ console.log('Submitting feedback payload:', feedbackPayload);
+
+ // Call the actual API
+ const response = await patientAPI.submitFeedback(feedbackPayload, user?.access_token);
+ console.log('update response', response);
+
+ if (!response.ok) {
+ throw new Error(response.problem || 'Failed to submit feedback');
+ }
+
+ // Show success message
+ setFeedbackResult({
+ type: 'success',
+ title: 'Feedback Submitted',
+ message: 'Your feedback has been recorded successfully.'
+ });
+ setShowFeedbackResultModal(true);
+ } catch (error: any) {
+ setFeedbackResult({
+ type: 'error',
+ title: 'Error',
+ message: error.message || 'Failed to submit feedback. Please try again.'
+ });
+ setShowFeedbackResultModal(true);
+ } finally {
+ setIsSubmittingFeedback(false);
+ }
+ }, [selectedSeriesForFeedback, selectedPrediction, feedbackText, isPositive, patientData?.patid]);
/**
- * Render Tab Navigation
+ * Handle Close Feedback Modal
*
- * Purpose: Render tab navigation for different sections
+ * Purpose: Close feedback modal and reset state
*/
- const renderTabNavigation = () => (
-
- {[
- { key: 'overview', label: 'Overview', icon: 'info' },
- { key: 'images', label: 'Images', icon: 'image', count: parsedData.series.length },
- { key: 'history', label: 'History', icon: 'clock' },
- ].map((tab) => (
- setActiveTab(tab.key as any)}
- >
-
-
- {tab.label}
-
- {tab.count !== undefined && (
-
- {tab.count}
-
- )}
-
- ))}
-
- );
+ const handleCloseFeedback = useCallback(() => {
+ setShowFeedbackModal(false);
+ setSelectedSeriesForFeedback(null);
+ setSelectedPrediction(null);
+ setFeedbackText('');
+ setIsPositive(null);
+ }, []);
/**
- * Render Overview Tab
+ * Handle Feedback Result Modal Close
*
- * Purpose: Render patient overview information
+ * Purpose: Close feedback result modal and reset form if success
*/
- const renderOverviewTab = () => (
-
- {/* Medical Case Information */}
-
- Case Information
-
-
- Case ID
- {medicalCase.id}
-
-
- Type
- {medicalCase.type}
-
-
- Created
-
- {new Date(medicalCase.created_at).toLocaleDateString()}
-
-
-
- Updated
-
- {new Date(medicalCase.updated_at).toLocaleDateString()}
-
-
-
-
+ const handleFeedbackResultClose = useCallback(() => {
+ setShowFeedbackResultModal(false);
+ setFeedbackResult(null);
+
+ // If it was a success, also close the feedback modal and reset form
+ if (feedbackResult?.type === 'success') {
+ setShowFeedbackModal(false);
+ setSelectedSeriesForFeedback(null);
+ setSelectedPrediction(null);
+ setFeedbackText('');
+ setIsPositive(null);
+ }
+ }, [feedbackResult?.type]);
- {/* Patient Details */}
-
- Patient Details
-
-
- Name
- {parsedData.patientDetails.Name || 'N/A'}
-
-
- Age
- {parsedData.patientDetails.PatAge || 'N/A'}
-
-
- Sex
- {parsedData.patientDetails.PatSex || 'N/A'}
-
-
- Status
- {parsedData.patientDetails.Status || 'N/A'}
-
-
- Institution
- {parsedData.patientDetails.InstName || 'N/A'}
-
-
- Modality
- {parsedData.patientDetails.Modality || 'N/A'}
-
-
-
-
- {/* Series Information */}
- {parsedData.series.length > 0 && (
-
- Imaging Series
-
-
- {parsedData.series.length} series available
-
-
- Total series: {parsedData.series.length}
-
-
-
- )}
-
- );
-
- /**
- * Render Images Tab
- *
- * Purpose: Render DICOM image gallery
- */
- const renderImagesTab = () => (
-
- {parsedData.series.length === 0 ? (
-
-
- No Images Available
-
- No DICOM images are currently available for this patient
-
-
- ) : (
-
- DICOM Images
-
- {parsedData.series.map((series, seriesIndex) => (
-
-
-
- Series {series.SeriesNum}: {series.SerDes || 'Unnamed Series'}
-
-
- {series.ImgTotalinSeries || '0'} images in Path • {series.ViePos || 'Unknown position'}
-
-
-
- {/* Series Details */}
-
-
- Series Number:
-
- {series.SeriesNum || 'N/A'}
-
-
-
- Total Images:
-
- {series.ImgTotalinSeries || '0'}
-
-
-
- Description:
-
- {series.SerDes || 'N/A'}
-
-
- {series.Path && Array.isArray(series.Path) && (
-
- Path Array:
-
- {series.Path.length} URLs available
-
-
- )}
- {series.ViePos && (
-
- View Position:
-
- {series.ViePos}
-
-
- )}
-
-
- {/* Series Image */}
- {series.pngpath ? (
- handleImagePress(seriesIndex)}
- >
-
-
- Series Image
-
-
- ) : (
-
-
- No Image Available
-
- )}
-
- ))}
-
- )}
-
- );
-
- /**
- * Render History Tab
- *
- * Purpose: Render patient medical history
- */
- const renderHistoryTab = () => (
-
-
- Medical History
-
-
-
- Case created on {new Date(medicalCase.created_at).toLocaleDateString()}
-
-
-
-
-
- Last updated on {new Date(medicalCase.updated_at).toLocaleDateString()}
-
-
-
-
-
- Status: {medicalCase.type} case
-
-
-
-
-
- Notes
-
- No additional notes available for this patient case.
-
-
-
- );
+ // ============================================================================
+ // UTILITY FUNCTIONS
+ // ============================================================================
/**
* Get Status Color
@@ -573,22 +470,577 @@ const PatientDetailsScreen: React.FC = ({ navigation,
* @param status - Patient status
*/
const getStatusColor = (status: string) => {
- switch (status) {
- case 'Critical':
- return theme.colors.error;
- case 'Emergency':
+ switch (status.toLowerCase()) {
+ case 'processed':
+ return theme.colors.success;
+ case 'pending':
return theme.colors.warning;
- case 'Routine':
+ case 'error':
+ return theme.colors.error;
+ default:
+ return theme.colors.info;
+ }
+ };
+
+ /**
+ * Get Clinical Urgency Color
+ *
+ * Purpose: Get appropriate color for clinical urgency
+ *
+ * @param urgency - Clinical urgency level
+ */
+ const getUrgencyColor = (urgency: string) => {
+ switch (urgency.toLowerCase()) {
+ case 'urgent':
+ return theme.colors.error;
+ case 'semi-urgent':
+ return theme.colors.warning;
+ case 'non-urgent':
return theme.colors.success;
default:
return theme.colors.info;
}
};
+ // ============================================================================
+ // RENDER HELPERS
+ // ============================================================================
+
+ /**
+ * Render Loading State
+ *
+ * Purpose: Render loading state while fetching data
+ */
+ const renderLoadingState = () => (
+
+
+ Loading patient data...
+
+ );
+
+ /**
+ * Render Error State
+ *
+ * Purpose: Render error state when API fails
+ */
+ const renderErrorState = () => (
+
+
+ Error Loading Patient Data
+ {error}
+
+ Retry
+
+
+ );
+
+ /**
+ * Render Patient Header
+ *
+ * Purpose: Render patient identification and status section
+ */
+ const renderPatientHeader = () => {
+ if (!patientData) return null;
+
+ const isCritical = patientData.patient_info.status === 'Error' ||
+ patientData.patient_info.report_status === 'Critical';
+
+ return (
+
+
+
+
+
+
+
+ {patientData.patient_info.name || 'Unknown Patient'}
+
+
+ ID: {patientData.patid}
+
+
+
+ {patientData.patient_info.age || 'N/A'} • {patientData.patient_info.sex || 'N/A'}
+
+
+ {patientData.patient_info.status}
+
+
+
+
+
+ {isCritical && (
+
+
+ EMERGENCY
+
+ )}
+
+ );
+ };
+
+ /**
+ * Render Tab Navigation
+ *
+ * Purpose: Render tab navigation for different sections
+ *
+ * Tab Structure:
+ * - Overview: Patient demographics and medical information
+ * - AI Analysis: Merged view of DICOM images and AI predictions (formerly separate Images and Predictions tabs)
+ * - History: Processing history and notes
+ */
+ const renderTabNavigation = () => {
+ if (!patientData) return null;
+
+ return (
+
+ {[
+ { key: 'overview', label: 'Overview', icon: 'info' },
+ { key: 'aiAnalysis', label: 'AI Analysis', icon: 'activity', count: patientData.series_summary.length },
+ { key: 'history', label: 'History', icon: 'clock' },
+ ].map((tab) => (
+ setActiveTab(tab.key as any)}
+ >
+
+
+ {tab.label}
+
+ {tab.count !== undefined && (
+
+ {tab.count}
+
+ )}
+
+ ))}
+
+ );
+ };
+
+ /**
+ * Render Overview Tab
+ *
+ * Purpose: Render patient overview information
+ */
+ const renderOverviewTab = () => {
+ if (!patientData) return null;
+
+ return (
+
+ {/* Patient Information */}
+
+ Patient Information
+
+
+ Patient ID
+ {patientData.patid}
+
+
+ Hospital ID
+ {patientData.hospital_id.substring(0, 8)}...
+
+
+ Name
+ {patientData.patient_info.name}
+
+
+ Age
+ {patientData.patient_info.age}
+
+
+ Sex
+ {patientData.patient_info.sex}
+
+
+ Date
+ {patientData.patient_info.date}
+
+
+
+
+ {/* Medical Information */}
+
+ Medical Information
+
+
+ Institution
+ {patientData.patient_info.institution}
+
+
+ Modality
+ {patientData.patient_info.modality}
+
+
+ Status
+ {patientData.patient_info.status}
+
+
+ Report Status
+ {patientData.patient_info.report_status}
+
+
+ File Name
+ {patientData.patient_info.file_name}
+
+
+ Frame Count
+ {patientData.patient_info.frame_count}
+
+
+
+
+ {/* Processing Information */}
+
+ Processing Information
+
+
+ First Processed
+
+ {new Date(patientData.first_processed_at).toLocaleDateString()}
+
+
+
+ Last Processed
+
+ {new Date(patientData.last_processed_at).toLocaleDateString()}
+
+
+
+ Total Predictions
+ {patientData.total_predictions}
+
+
+ Series Count
+ {patientData.series_summary.length}
+
+
+
+
+ );
+ };
+
+ /**
+ * Render AI Analysis Tab
+ *
+ * Purpose: Render AI predictions and findings alongside their related images
+ *
+ * Features:
+ * - Merged view of DICOM images and AI predictions
+ * - Side-by-side display on larger screens, stacked on mobile
+ * - Summary statistics at the top
+ * - Series-based organization with integrated image and prediction data
+ * - Responsive design for different screen sizes
+ * - Feedback system for each series allowing physicians to provide clinical insights
+ */
+ const renderAIAnalysisTab = () => {
+ if (!patientData) return null;
+
+ if (patientData.series_summary.length === 0) {
+ return (
+
+
+
+ No Images Available
+
+ No DICOM images are currently available for this patient
+
+
+
+ );
+ }
+
+ return (
+
+
+ AI Analysis & DICOM Images
+
+
+ {/* Summary Statistics */}
+
+
+
+ Total Series
+ {patientData.series_summary.length}
+
+
+
+ AI Predictions
+ {patientData.total_predictions}
+
+
+
+ Processing Status
+ {patientData.patient_info.status}
+
+
+
+ {patientData.series_summary.map((series, seriesIndex) => {
+ // Get predictions for this series
+ {console.log('series.png_preview', series)}
+ const seriesPredictions = patientData.predictions_by_series[series.series_num] || [];
+ const hasPredictions = seriesPredictions.length > 0;
+
+ return (
+
+ {/* Series Header */}
+
+
+
+ Series {series.series_num}: {series.series_description}
+
+ handleOpenFeedback(series, seriesPredictions[0])}
+ activeOpacity={0.7}
+ >
+
+ Feedback
+
+
+
+ {series.total_images} images • {series.modality} modality
+ {hasPredictions && ` • ${seriesPredictions.length} AI predictions`}
+
+
+
+ {/* Series Details */}
+
+
+ Series Number:
+ {series.series_num}
+
+
+ Description:
+
+ {series.series_description}
+
+
+
+ Total Images:
+ {series.total_images}
+
+
+ Modality:
+ {series.modality}
+
+
+ AI Predictions:
+
+ {hasPredictions ? `${seriesPredictions.length} found` : 'None'}
+
+
+
+
+ {/* Image and Predictions Row */}
+
+ {/* DICOM Image */}
+
+
+ DICOM Preview
+
+ {seriesPredictions[0]?.preview ? (
+ handleImagePress(seriesIndex)}
+ >
+
+
+ Series {series.series_num}
+
+
+ ) : (
+
+
+
+ No Preview Available
+
+ )}
+
+
+ {/* AI Predictions */}
+
+
+ AI Analysis Results
+
+
+ {hasPredictions ? (
+ seriesPredictions.map((prediction) => (
+
+
+ {prediction.prediction.label}
+
+
+ {prediction.prediction.clinical_urgency}
+
+
+
+
+
+
+ Finding Type:
+
+ {prediction.prediction.finding_type}
+
+
+
+ Confidence:
+
+ {(prediction.prediction.confidence_score * 100).toFixed(1)}%
+
+
+
+ Category:
+
+ {prediction.prediction.finding_category}
+
+
+
+ Severity:
+
+ {prediction.prediction.primary_severity}
+
+
+
+ Location:
+
+ {prediction.prediction.anatomical_location}
+
+
+
+
+
+ Processed: {new Date(prediction.processed_at).toLocaleDateString()}
+
+
+ ))
+ ) : (
+
+
+ No AI predictions for this series
+
+ )}
+
+
+
+ );
+ })}
+
+ );
+ };
+
+ /**
+ * Render History Tab
+ *
+ * Purpose: Render patient medical history
+ */
+ const renderHistoryTab = () => {
+ if (!patientData) return null;
+
+ return (
+
+
+ Processing History
+
+
+
+ First processed on {new Date(patientData.first_processed_at).toLocaleDateString()}
+
+
+
+
+
+ Last updated on {new Date(patientData.last_processed_at).toLocaleDateString()}
+
+
+
+
+
+ Status: {patientData.patient_info.status} case
+
+
+
+
+
+ Total AI predictions: {patientData.total_predictions}
+
+
+
+
+
+ Notes
+
+ Patient case processed with {patientData.series_summary.length} DICOM series.
+ AI analysis completed with {patientData.total_predictions} predictions.
+
+
+
+ );
+ };
+
// ============================================================================
// MAIN RENDER
// ============================================================================
+ if (isLoading) {
+ return (
+
+
+ {renderLoadingState()}
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ {renderErrorState()}
+
+ );
+ }
+
+ if (!patientData) {
+ return (
+
+
+
+
+ Patient Not Found
+ The requested patient data could not be found.
+
+
+ );
+ }
+
return (
@@ -637,22 +1089,160 @@ const PatientDetailsScreen: React.FC = ({ navigation,
/>
}
>
+ {/* Overview Tab: Patient demographics and medical information */}
{activeTab === 'overview' && renderOverviewTab()}
- {activeTab === 'images' && renderImagesTab()}
+
+ {/* AI Analysis Tab: Merged view of DICOM images and AI predictions */}
+ {activeTab === 'aiAnalysis' && renderAIAnalysisTab()}
+
+ {/* History Tab: Processing history and notes */}
{activeTab === 'history' && renderHistoryTab()}
- {/* Full-Screen Image Viewer */}
-
-
-
+ {/* Feedback Modal: Allows physicians to provide clinical feedback on AI predictions and DICOM images */}
+ {showFeedbackModal && selectedSeriesForFeedback && (
+
+
+
+ Provide Feedback
+
+
+
+
+
+
+
+ {/*
+
+ Series {selectedSeriesForFeedback.series_num}: {selectedSeriesForFeedback.series_description}
+
+
+ {selectedSeriesForFeedback.modality} • {selectedSeriesForFeedback.total_images} images
+
+ */}
+
+ {/* Series and Prediction Info */}
+ {selectedPrediction && (
+
+
+ AI Prediction: {selectedPrediction.prediction.label}
+
+
+ Confidence: {(selectedPrediction.prediction.confidence_score * 100).toFixed(1)}% •
+ Type: {selectedPrediction.prediction.finding_type}
+
+
+ )}
+
+ {/* Prediction Accuracy Selection */}
+
+ Is this prediction accurate?
+
+ {[
+ { key: 'true', label: 'Yes (Positive)', color: theme.colors.success, icon: 'check-circle', value: true },
+ { key: 'false', label: 'No (Negative)', color: theme.colors.error, icon: 'x-circle', value: false }
+ ].map((option) => (
+ setIsPositive(option.value)}
+ style={[
+ styles.predictionAccuracyButton,
+ isPositive === option.value && styles.predictionAccuracyButtonActive,
+ { borderColor: option.color }
+ ]}
+ >
+
+
+ {option.label}
+
+
+ ))}
+
+
+
+ {/* Feedback Text Input */}
+
+ Your Feedback
+
+
+
+
+
+
+ Cancel
+
+
+ Submit Feedback
+
+
+
+
+ )}
+
+ {/* Feedback Result Modal: Shows success/error messages for feedback submission */}
+ {showFeedbackResultModal && feedbackResult && (
+
+
+
+
+
+ {feedbackResult.title}
+
+
+
+
+
+ {feedbackResult.message}
+
+
+
+
+
+ OK
+
+
+
+
+ )}
);
};
@@ -893,12 +1483,33 @@ const styles = StyleSheet.create({
seriesHeader: {
marginBottom: theme.spacing.md,
},
+ seriesHeaderTop: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 4,
+ },
seriesTitle: {
fontSize: 16,
fontWeight: 'bold',
color: theme.colors.textPrimary,
fontFamily: theme.typography.fontFamily.bold,
- marginBottom: 4,
+ },
+ feedbackButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: theme.colors.backgroundAlt,
+ paddingHorizontal: 10,
+ paddingVertical: 6,
+ borderRadius: 16,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ feedbackButtonText: {
+ fontSize: 12,
+ color: theme.colors.primary,
+ fontFamily: theme.typography.fontFamily.medium,
+ marginLeft: 4,
},
seriesMeta: {
fontSize: 14,
@@ -1031,12 +1642,480 @@ const styles = StyleSheet.create({
flex: 2,
textAlign: 'right',
},
+
+ // Prediction Styles
+ predictionSeries: {
+ marginBottom: theme.spacing.lg,
+ },
+ predictionSeriesTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ marginBottom: theme.spacing.sm,
+ },
+ predictionCard: {
+ backgroundColor: theme.colors.backgroundAlt,
+ borderRadius: 8,
+ padding: theme.spacing.md,
+ marginBottom: theme.spacing.sm,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ predictionHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: theme.spacing.sm,
+ },
+ predictionLabel: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ },
+ urgencyBadge: {
+ paddingHorizontal: 10,
+ paddingVertical: 4,
+ borderRadius: 12,
+ },
+ urgencyText: {
+ fontSize: 10,
+ fontWeight: 'bold',
+ color: theme.colors.background,
+ textTransform: 'uppercase',
+ },
+ predictionDetails: {
+ marginBottom: theme.spacing.sm,
+ },
+ predictionDetailItem: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ marginBottom: theme.spacing.xs,
+ },
+ predictionDetailLabel: {
+ fontSize: 12,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.medium,
+ flex: 1,
+ },
+ predictionDetailValue: {
+ fontSize: 12,
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.regular,
+ flex: 2,
+ textAlign: 'right',
+ },
+ predictionTimestamp: {
+ fontSize: 12,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.regular,
+ textAlign: 'right',
+ },
+
+ // Loading and Error States
+ loadingContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: theme.spacing.lg,
+ },
+ loadingText: {
+ marginTop: theme.spacing.sm,
+ fontSize: 16,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.regular,
+ },
+ errorContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: theme.spacing.lg,
+ },
+ errorTitle: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ marginTop: theme.spacing.md,
+ marginBottom: theme.spacing.sm,
+ },
+ errorMessage: {
+ fontSize: 14,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.regular,
+ textAlign: 'center',
+ marginBottom: theme.spacing.md,
+ },
+ retryButton: {
+ backgroundColor: theme.colors.primary,
+ paddingVertical: theme.spacing.md,
+ paddingHorizontal: theme.spacing.lg,
+ borderRadius: 8,
+ shadowColor: theme.colors.primary,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.3,
+ shadowRadius: 4,
+ elevation: 4,
+ },
+ retryButtonText: {
+ color: theme.colors.background,
+ fontSize: 16,
+ fontWeight: 'bold',
+ fontFamily: theme.typography.fontFamily.bold,
+ },
+
+ // New styles for AI Analysis Tab
+ imagePredictionsRow: {
+ flexDirection: screenWidth > 600 ? 'row' : 'column',
+ justifyContent: 'space-between',
+ marginTop: theme.spacing.md,
+ },
+ imageSection: {
+ flex: 1,
+ marginRight: screenWidth > 600 ? theme.spacing.md : 0,
+ marginBottom: screenWidth > 600 ? 0 : theme.spacing.md,
+ },
+ imageSectionTitle: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ marginBottom: theme.spacing.sm,
+ },
+ predictionsSection: {
+ flex: 1,
+ },
+ predictionsSectionTitle: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ marginBottom: theme.spacing.sm,
+ },
+ noPredictionsPlaceholder: {
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: theme.spacing.md,
+ },
+ noPredictionsText: {
+ fontSize: 14,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.regular,
+ marginTop: theme.spacing.sm,
+ textAlign: 'center',
+ },
+
+ // New styles for AI Analysis Tab
+ analysisSummary: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ backgroundColor: theme.colors.backgroundAlt,
+ borderRadius: 8,
+ paddingVertical: theme.spacing.md,
+ paddingHorizontal: theme.spacing.sm,
+ marginBottom: theme.spacing.md,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.1,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ summaryItem: {
+ alignItems: 'center',
+ },
+ summaryLabel: {
+ fontSize: 12,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.medium,
+ marginTop: theme.spacing.xs,
+ },
+ summaryValue: {
+ fontSize: 18,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ marginTop: theme.spacing.xs,
+ },
+
+ // Feedback Modal Styles
+ modalOverlay: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ backgroundColor: 'rgba(0, 0, 0, 0.7)',
+ justifyContent: 'center',
+ alignItems: 'center',
+ zIndex: 1000,
+ },
+ feedbackModal: {
+ backgroundColor: theme.colors.background,
+ borderRadius: 12,
+ width: '90%',
+ maxWidth: 450,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.3,
+ shadowRadius: 10,
+ elevation: 10,
+ },
+ modalHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: theme.spacing.md,
+ borderBottomWidth: 1,
+ borderBottomColor: theme.colors.border,
+ },
+ modalTitle: {
+ fontSize: 20,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ },
+ closeButton: {
+ padding: theme.spacing.sm,
+ },
+ modalContent: {
+ padding: theme.spacing.md,
+ },
+ feedbackSeriesInfo: {
+ marginBottom: theme.spacing.md,
+ },
+ feedbackSeriesTitle: {
+ fontSize: 16,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ marginBottom: theme.spacing.xs,
+ },
+ feedbackSeriesMeta: {
+ fontSize: 12,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.regular,
+ },
+ feedbackSection: {
+ marginBottom: theme.spacing.md,
+ },
+ feedbackSectionTitle: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ marginBottom: theme.spacing.sm,
+ },
+ feedbackTypeContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ marginBottom: theme.spacing.sm,
+ },
+ feedbackTypeButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: theme.spacing.sm,
+ paddingHorizontal: theme.spacing.md,
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ feedbackTypeButtonActive: {
+ borderColor: theme.colors.primary,
+ backgroundColor: theme.colors.primary,
+ },
+ feedbackTypeButtonText: {
+ fontSize: 14,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.medium,
+ marginLeft: theme.spacing.sm,
+ },
+ feedbackTypeButtonTextActive: {
+ color: theme.colors.background,
+ fontFamily: theme.typography.fontFamily.bold,
+ },
+ priorityContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ marginBottom: theme.spacing.sm,
+ },
+ priorityButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: theme.spacing.sm,
+ paddingHorizontal: theme.spacing.md,
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ priorityButtonActive: {
+ borderColor: theme.colors.primary,
+ backgroundColor: theme.colors.primary,
+ },
+ priorityIndicator: {
+ width: 10,
+ height: 10,
+ borderRadius: 5,
+ marginRight: theme.spacing.sm,
+ },
+ priorityButtonText: {
+ fontSize: 14,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.medium,
+ },
+ priorityButtonTextActive: {
+ color: theme.colors.background,
+ fontFamily: theme.typography.fontFamily.bold,
+ },
+ feedbackTextInput: {
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ borderRadius: 8,
+ padding: theme.spacing.md,
+ fontSize: 14,
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.regular,
+ minHeight: 100,
+ textAlignVertical: 'top',
+ },
+ modalFooter: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ padding: theme.spacing.md,
+ borderTopWidth: 1,
+ borderTopColor: theme.colors.border,
+ },
+
+ // Feedback Prediction Info Styles
+ feedbackPredictionInfo: {
+ // marginTop: theme.spacing.sm,
+ paddingTop: theme.spacing.md,
+ marginBottom: theme.spacing.sm,
+ // borderTopWidth: 1,
+ // borderTopColor: theme.colors.border,
+ },
+ feedbackPredictionTitle: {
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.bold,
+ marginBottom: theme.spacing.xs,
+ },
+ feedbackPredictionMeta: {
+ fontSize: 12,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.regular,
+ },
+
+ // Prediction Accuracy Selection Styles
+ predictionAccuracyContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ marginBottom: theme.spacing.sm,
+ },
+ predictionAccuracyButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: theme.spacing.sm,
+ paddingHorizontal: theme.spacing.md,
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ minWidth: 120,
+ justifyContent: 'center',
+ },
+ predictionAccuracyButtonActive: {
+ borderColor: theme.colors.primary,
+ backgroundColor: theme.colors.primary,
+ },
+ predictionAccuracyButtonText: {
+ fontSize: 14,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.medium,
+ marginLeft: theme.spacing.sm,
+ },
+ predictionAccuracyButtonTextActive: {
+ color: theme.colors.background,
+ fontFamily: theme.typography.fontFamily.bold,
+ },
+ cancelButton: {
+ paddingVertical: theme.spacing.md,
+ paddingHorizontal: theme.spacing.lg,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ cancelButtonText: {
+ fontSize: 16,
+ color: theme.colors.textSecondary,
+ fontFamily: theme.typography.fontFamily.medium,
+ },
+ submitButton: {
+ paddingVertical: theme.spacing.md,
+ paddingHorizontal: theme.spacing.lg,
+ borderRadius: 8,
+ backgroundColor: theme.colors.primary,
+ shadowColor: theme.colors.primary,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.3,
+ shadowRadius: 4,
+ elevation: 4,
+ },
+ submitButtonDisabled: {
+ backgroundColor: theme.colors.textMuted,
+ opacity: 0.7,
+ },
+ submitButtonText: {
+ color: theme.colors.background,
+ fontSize: 16,
+ fontWeight: 'bold',
+ fontFamily: theme.typography.fontFamily.bold,
+ },
+
+ // Feedback Result Modal Styles
+ feedbackResultModal: {
+ backgroundColor: theme.colors.background,
+ borderRadius: 16,
+ padding: 0,
+ width: '90%',
+ maxWidth: 400,
+ shadowColor: '#000000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.15,
+ shadowRadius: 8,
+ elevation: 8,
+ },
+ feedbackResultMessage: {
+ fontSize: 16,
+ color: theme.colors.textPrimary,
+ fontFamily: theme.typography.fontFamily.regular,
+ textAlign: 'center',
+ lineHeight: 24,
+ paddingHorizontal: theme.spacing.md,
+ },
+ okButton: {
+ paddingVertical: theme.spacing.md,
+ paddingHorizontal: theme.spacing.xl,
+ borderRadius: 8,
+ backgroundColor: theme.colors.primary,
+ shadowColor: theme.colors.primary,
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.3,
+ shadowRadius: 4,
+ elevation: 4,
+ minWidth: 100,
+ alignItems: 'center',
+ },
+ okButtonText: {
+ color: theme.colors.background,
+ fontSize: 16,
+ fontWeight: 'bold',
+ fontFamily: theme.typography.fontFamily.bold,
+ },
});
export default PatientDetailsScreen;
-
-/*
- * End of File: PatientDetailsScreen.tsx
- * Design & Developed by Tech4Biz Solutions
- * Copyright (c) Spurrin Innovations. All rights reserved.
- */
diff --git a/app/modules/PatientCare/screens/PatientsScreen.tsx b/app/modules/PatientCare/screens/PatientsScreen.tsx
index 1e37cf7..85ce8e7 100644
--- a/app/modules/PatientCare/screens/PatientsScreen.tsx
+++ b/app/modules/PatientCare/screens/PatientsScreen.tsx
@@ -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 = ({ 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 = ({ 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 = ({ 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 = ({ 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 }) => (
handlePatientPress(item)}
onEmergencyPress={() => handleEmergencyAlert(item)}
/>
+ ), [handlePatientPress, handleEmergencyAlert]);
+
+ /**
+ * Render Header
+ *
+ * Purpose: Render the screen header with title and action buttons
+ */
+ const renderHeader = () => (
+
+
+ Patients
+
+ {filteredPatients.length} of {patients?.length || 0} patients
+
+
+
+
+ {
+ // TODO: Implement sort modal
+ }}
+ >
+ Sort
+
+
+ {
+ // TODO: Implement filter modal
+ }}
+ >
+ Filter
+
+
+
);
/**
@@ -318,168 +242,143 @@ const PatientsScreen: React.FC = ({ navigation }) => {
*
* Purpose: Render empty state when no patients found
*/
- const renderEmptyState = () => {
- if (isLoading) return null;
-
- return (
-
- );
- };
-
-
-
- /**
- * Render Loading State
- *
- * Purpose: Render loading state during initial fetch
- */
- if (isLoading && patients.length === 0) {
- return (
-
-
-
-
- );
- }
+ const renderEmptyState = () => (
+
+ );
// ============================================================================
// MAIN RENDER
// ============================================================================
+ if (error && !isLoading) {
+ return (
+
+
+
+ Error Loading Patients
+ {error}
+
+ Retry
+
+
+
+ );
+ }
+
return (
- {/* Fixed Header */}
-
-
- navigation.goBack()}
- >
-
-
-
- Patients
- Emergency Department
-
-
+ {/* Header */}
+ {renderHeader()}
+
+ {/* Search and Filters */}
+
+
-
-
-
+
+
+ {/* Loading State */}
+ {isLoading && patients.length === 0 && (
+
+
+
+ )}
+
+ {/* Error State */}
+ {error && patients.length === 0 && (
+
+
+
+ )}
+
+ {/* Empty State */}
+ {!isLoading && !error && patients.length === 0 && (
+
+
+
+ )}
+
+ {/* No Results State */}
+ {!isLoading && !error && patients.length > 0 && filteredPatients.length === 0 && (
+
+ {
+ handleSearch('');
+ handleFilterChange('all');
+ }}
+ />
+
+ )}
+
+ {/* Patient List */}
+ {!isLoading && !error && filteredPatients.length > 0 && (
+ item.patid}
+ contentContainerStyle={styles.listContainer}
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+
-
-
- {
- // TODO: Implement notifications screen
- Alert.alert('Notifications', 'Notifications feature coming soon');
- }}
- >
-
- {/* Notification badge */}
-
- 3
+ }
+ ListFooterComponent={
+
+
+ Showing {filteredPatients.length} of {patients.length} patients
+
-
-
-
-
- {/* Fixed Search and Filter Section */}
-
- {/* Search Bar */}
-
- setShowSortModal(true)}
- />
-
-
- {/* Filter Tabs */}
-
- p.type === 'Critical').length,
- Routine: patients.filter((p: MedicalCase) => p.type === 'Routine').length,
- Emergency: patients.filter((p: MedicalCase) => p.type === 'Emergency').length,
- }}
- />
-
-
- {/* Results Summary */}
-
-
-
-
- {filteredPatients.length} patient{filteredPatients.length !== 1 ? 's' : ''} found
-
-
-
-
-
- Sorted by {sortBy}
-
-
-
-
-
- {/* Scrollable Patient List Only */}
- index.toString()}
- ListEmptyComponent={renderEmptyState}
- contentContainerStyle={[
- styles.listContent,
- filteredPatients.length === 0 && styles.emptyListContent
- ]}
- showsVerticalScrollIndicator={false}
- refreshControl={
-
- }
- // 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 */}
);
};
@@ -489,6 +388,7 @@ const PatientsScreen: React.FC = ({ 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,
},
});
diff --git a/app/modules/PatientCare/services/patientAPI.ts b/app/modules/PatientCare/services/patientAPI.ts
index 0f86db4..ddd3017 100644
--- a/app/modules/PatientCare/services/patientAPI.ts
+++ b/app/modules/PatientCare/services/patientAPI.ts
@@ -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
*
diff --git a/app/modules/Settings/components/SettingsHeader.tsx b/app/modules/Settings/components/SettingsHeader.tsx
index 02bb2c5..0318b76 100644
--- a/app/modules/Settings/components/SettingsHeader.tsx
+++ b/app/modules/Settings/components/SettingsHeader.tsx
@@ -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 = ({ title }) => {
+export const SettingsHeader: React.FC = ({
+ title,
+ showBackButton = false,
+ onBackPress
+}) => {
return (
+ {showBackButton && onBackPress && (
+
+
+
+ )}
{title}
);
@@ -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,
},
});
diff --git a/app/modules/Settings/navigation/SettingsStackNavigator.tsx b/app/modules/Settings/navigation/SettingsStackNavigator.tsx
index ec940ac..d4999af 100644
--- a/app/modules/Settings/navigation/SettingsStackNavigator.tsx
+++ b/app/modules/Settings/navigation/SettingsStackNavigator.tsx
@@ -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();
@@ -78,6 +79,38 @@ const SettingsStackNavigator: React.FC = () => {
headerShown: false, // Hide header for main settings screen
}}
/>
+
+
+
+ {/* */}
);
};
diff --git a/app/modules/Settings/navigation/navigationTypes.ts b/app/modules/Settings/navigation/navigationTypes.ts
index fee801c..e484f63 100644
--- a/app/modules/Settings/navigation/navigationTypes.ts
+++ b/app/modules/Settings/navigation/navigationTypes.ts
@@ -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
diff --git a/app/modules/Settings/screens/AppInfoScreen.tsx b/app/modules/Settings/screens/AppInfoScreen.tsx
new file mode 100644
index 0000000..319abf1
--- /dev/null
+++ b/app/modules/Settings/screens/AppInfoScreen.tsx
@@ -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 = ({
+ 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 (
+
+ {/* App info header with back button */}
+ navigation.goBack()}
+ />
+
+ {/* Scrollable app information content */}
+
+ {/* App logo and basic info section */}
+
+
+
+ NS
+
+
+
+ {appInfo.name}
+ Version {appInfo.version}
+ Build {appInfo.buildNumber}
+ {appInfo.releaseDate}
+
+
+ {/* App description section */}
+
+ {appDescription.title}
+ {appDescription.description}
+
+ Key Features:
+ {appDescription.features.map((feature, index) => (
+
+
+ {feature}
+
+ ))}
+
+
+ {/* Version and build details section */}
+
+ Technical Information
+
+
+ App Version:
+ {appInfo.version}
+
+
+
+ Build Number:
+ {appInfo.buildNumber}
+
+
+
+ Release Date:
+ {appInfo.releaseDate}
+
+
+
+ Developer:
+ {appInfo.developer}
+
+
+
+ {/* Support and contact section */}
+
+ Support & Contact
+
+
+ Email Support:
+ {legalInfo.supportEmail}
+
+
+
+ Phone Support:
+ {legalInfo.supportPhone}
+
+
+ handleExternalLink(legalInfo.website)}
+ activeOpacity={0.7}
+ >
+ Website:
+ {legalInfo.website}
+
+
+
+ {/* Legal information section */}
+
+ Legal Information
+
+ handleExternalLink(legalInfo.privacyPolicy)}
+ activeOpacity={0.7}
+ >
+ Privacy Policy
+
+
+
+ handleExternalLink(legalInfo.termsOfService)}
+ activeOpacity={0.7}
+ >
+ Terms of Service
+
+
+
+
+ {/* Copyright section */}
+
+ {appInfo.copyright}
+
+
+ {/* Bottom spacing for tab bar */}
+
+
+
+ );
+};
+
+// ============================================================================
+// 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.
+ */
diff --git a/app/modules/Settings/screens/ChangePasswordScreen.tsx b/app/modules/Settings/screens/ChangePasswordScreen.tsx
new file mode 100644
index 0000000..d349ba7
--- /dev/null
+++ b/app/modules/Settings/screens/ChangePasswordScreen.tsx
@@ -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 = ({
+ navigation,
+}) => {
+ // ============================================================================
+ // REDUX STATE
+ // ============================================================================
+
+ const dispatch = useAppDispatch();
+
+ // ============================================================================
+ // LOCAL STATE
+ // ============================================================================
+
+ const [formData, setFormData] = useState({
+ currentPassword: '',
+ newPassword: '',
+ confirmPassword: '',
+ });
+
+ const [errors, setErrors] = useState({});
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [showCurrentPassword, setShowCurrentPassword] = useState(false);
+ const [showNewPassword, setShowNewPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
+ const [passwordStrength, setPasswordStrength] = useState({
+ 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 (
+
+ {/* Header with back button */}
+
+
+ {/* Scrollable form content */}
+
+ {/* Password requirements info */}
+
+ Password Requirements
+
+ Your new password must meet the following requirements to ensure security:
+
+
+ • At least 8 characters long
+ • Contains uppercase and lowercase letters
+ • Contains at least one number
+ • Contains at least one special character
+
+
+
+ {/* Password change form */}
+
+ Change Password
+
+ {/* Current Password Input */}
+
+ Current Password *
+
+ handleInputChange('currentPassword', value)}
+ onBlur={() => handleInputBlur('currentPassword')}
+ placeholder="Enter your current password"
+ placeholderTextColor={theme.colors.textMuted}
+ secureTextEntry={!showCurrentPassword}
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+ togglePasswordVisibility('current')}
+ activeOpacity={0.7}
+ >
+
+
+
+ {errors.currentPassword && (
+ {errors.currentPassword}
+ )}
+
+
+ {/* New Password Input */}
+
+ New Password *
+
+ handleInputChange('newPassword', value)}
+ onBlur={() => handleInputBlur('newPassword')}
+ placeholder="Enter your new password"
+ placeholderTextColor={theme.colors.textMuted}
+ secureTextEntry={!showNewPassword}
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+ togglePasswordVisibility('new')}
+ activeOpacity={0.7}
+ >
+
+
+
+ {errors.newPassword && (
+ {errors.newPassword}
+ )}
+
+
+ {/* Password Strength Indicator */}
+ {formData.newPassword.length > 0 && (
+
+ Password Strength:
+
+
+
+
+ {passwordStrength.label}
+
+
+ )}
+
+ {/* Password Requirements Check */}
+ {formData.newPassword.length > 0 && (
+
+ {passwordStrength.requirements.map((requirement, index) => (
+
+ {requirement}
+
+ ))}
+
+ )}
+
+ {/* Confirm Password Input */}
+
+ Confirm New Password *
+
+ handleInputChange('confirmPassword', value)}
+ onBlur={() => handleInputBlur('confirmPassword')}
+ placeholder="Confirm your new password"
+ placeholderTextColor={theme.colors.textMuted}
+ secureTextEntry={!showConfirmPassword}
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+ togglePasswordVisibility('confirm')}
+ activeOpacity={0.7}
+ >
+
+
+
+ {errors.confirmPassword && (
+ {errors.confirmPassword}
+ )}
+
+
+
+ {/* Action buttons */}
+
+
+
+ {isSubmitting ? 'Changing Password...' : 'Change Password'}
+
+
+
+
+ Cancel
+
+
+
+ {/* Bottom spacing for tab bar */}
+
+
+
+ );
+};
+
+// ============================================================================
+// 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.
+ */
diff --git a/app/modules/Settings/screens/EditProfileScreen.tsx b/app/modules/Settings/screens/EditProfileScreen.tsx
new file mode 100644
index 0000000..ff3bda9
--- /dev/null
+++ b/app/modules/Settings/screens/EditProfileScreen.tsx
@@ -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 = ({
+ 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({
+ firstName: '',
+ lastName: '',
+ displayName: '',
+ });
+
+ const [errors, setErrors] = useState({});
+ 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 (
+
+ {/* Header with back button */}
+
+
+ {/* Scrollable form content */}
+
+ {/* Current email display (read-only) */}
+
+ Account Information
+
+ Email Address
+
+ {currentEmail}
+
+
+
+
+ Email address cannot be changed
+
+
+
+ {/* Profile form section */}
+
+ Personal Information
+
+ {/* First Name Input */}
+
+ First Name *
+ handleInputChange('firstName', value)}
+ onBlur={() => handleInputBlur('firstName')}
+ placeholder="Enter your first name"
+ placeholderTextColor={theme.colors.textMuted}
+ autoCapitalize="words"
+ autoCorrect={false}
+ maxLength={50}
+ />
+ {errors.firstName && (
+ {errors.firstName}
+ )}
+
+
+ {/* Last Name Input */}
+
+ Last Name *
+ handleInputChange('lastName', value)}
+ onBlur={() => handleInputBlur('lastName')}
+ placeholder="Enter your last name"
+ placeholderTextColor={theme.colors.textMuted}
+ autoCapitalize="words"
+ autoCorrect={false}
+ maxLength={50}
+ />
+ {errors.lastName && (
+ {errors.lastName}
+ )}
+
+
+ {/* Display Name Input */}
+
+ Display Name *
+ handleInputChange('displayName', value)}
+ onBlur={() => handleInputBlur('displayName')}
+ placeholder="Enter your display name"
+ placeholderTextColor={theme.colors.textMuted}
+ autoCapitalize="words"
+ autoCorrect={false}
+ maxLength={30}
+ />
+ {errors.displayName && (
+ {errors.displayName}
+ )}
+
+
+
+ {/* Action buttons */}
+
+
+
+ {isSubmitting ? 'Updating...' : 'Update Profile'}
+
+
+
+
+ Cancel
+
+
+
+ {/* Bottom spacing for tab bar */}
+
+
+
+ );
+};
+
+// ============================================================================
+// 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.
+ */
diff --git a/app/modules/Settings/screens/SettingsScreen.tsx b/app/modules/Settings/screens/SettingsScreen.tsx
index 5a6a846..291bfe6 100644
--- a/app/modules/Settings/screens/SettingsScreen.tsx
+++ b/app/modules/Settings/screens/SettingsScreen.tsx
@@ -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 = ({
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 = ({
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 = ({
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 = ({
*/
useEffect(() => {
setSettingsSections(generateSettingsSections());
- }, [user, notificationPreferences, dashboardSettings]);
+ }, [user, dashboardSettings]);
// ============================================================================
// EVENT HANDLERS
@@ -310,17 +231,40 @@ export const SettingsScreen: React.FC = ({
* @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 = ({
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 = ({
{/* Profile card section */}
{user && (
-
-
-
- {user.profile_photo_url ? (
-
- ) : (
-
-
- {user.first_name.charAt(0)}{user.last_name.charAt(0)}
-
-
- )}
-
-
-
-
- {user.display_name || `${user.first_name} ${user.last_name}`}
-
- {user.email}
- Physician
-
-
-
- Edit
-
+
+
+ {user.profile_photo_url ? (
+
+ ) : (
+
+
+ {user.first_name.charAt(0)}{user.last_name.charAt(0)}
+
+
+ )}
-
+
+
+
+ {user.display_name || `${user.first_name} ${user.last_name}`}
+
+ {user.email}
+ Radiologist
+
+
)}
@@ -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,
- },
});
/*
diff --git a/app/modules/Settings/screens/index.ts b/app/modules/Settings/screens/index.ts
new file mode 100644
index 0000000..70e21ff
--- /dev/null
+++ b/app/modules/Settings/screens/index.ts
@@ -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.
+ */
diff --git a/app/navigation/MainTabNavigator.tsx b/app/navigation/MainTabNavigator.tsx
index f36db9f..acc9602 100644
--- a/app/navigation/MainTabNavigator.tsx
+++ b/app/navigation/MainTabNavigator.tsx
@@ -99,7 +99,7 @@ export const MainTabNavigator: React.FC = () => {
/>
{/* AI Predictions Tab - AI-powered medical predictions */}
- {
),
headerShown: false,
}}
- />
+ /> */}
{/* Reports Tab - Medical documentation */}
{/* {
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}`);
}