diff --git a/android/app/src/main/assets/dicom-viewer.html b/android/app/src/main/assets/dicom-viewer.html new file mode 100644 index 0000000..0f7adf1 --- /dev/null +++ b/android/app/src/main/assets/dicom-viewer.html @@ -0,0 +1,733 @@ + + + + + +DICOM Viewer - Fullscreen Mobile Friendly + + + + +
+
+ + + + +
+
+ + + + +
+
+ + + +
+
+
+
+ + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 1d5d02a..40796b0 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index 1d5d02a..40796b0 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index fed7c12..9a4f5b0 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index fed7c12..9a4f5b0 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 0d78f7e..d27846a 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index 0d78f7e..d27846a 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 8ef759a..0f52355 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 8ef759a..0f52355 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index b4c03d7..6065ff3 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png index b4c03d7..6065ff3 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/playstore.png b/android/app/src/main/res/playstore.png index bbab458..e503351 100644 Binary files a/android/app/src/main/res/playstore.png and b/android/app/src/main/res/playstore.png differ diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index c4c180d..9139f1c 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - Radiologist + Physician diff --git a/app/assets/dicom/dicom-viewer.html b/app/assets/dicom/dicom-viewer.html index 4e128a1..0f7adf1 100644 --- a/app/assets/dicom/dicom-viewer.html +++ b/app/assets/dicom/dicom-viewer.html @@ -2,376 +2,429 @@ - -DICOM Viewer - Mobile Friendly + +DICOM Viewer - Fullscreen Mobile Friendly -
-

DICOM Viewer

-
- Ready to load DICOM files - Loading... +
+
+ + + -
-
-
- - -
- -
- - - -
- -
-
- -
No images
- +
+
+ + + +
+
+ + + +
+
-
-
-
🩻
-
DICOM Viewer
-
No image loaded
DICOM files will be loaded from parent component
-
-
+ + + + + + @@ -382,345 +435,299 @@ diff --git a/app/assets/dicom/test-dicom-viewer.html b/app/assets/dicom/test-dicom-viewer.html index 0f7c363..84a39b5 100644 --- a/app/assets/dicom/test-dicom-viewer.html +++ b/app/assets/dicom/test-dicom-viewer.html @@ -1,443 +1,96 @@ - + - DICOM Viewer Test + Dummy Page -
-

DICOM Viewer Test

-

Test the DICOM viewer functionality in your browser before using it in React Native.

- -
-

Sample DICOM URLs

-
-
- Sample 1:
- LIDC-IDRI-0001 -
-
- Sample 2:
- LIDC-IDRI-0001 -
-
- Sample 3:
- LIDC-IDRI-0001 -
-
-
+
+

Welcome to the Dummy Page

+

This is just a placeholder website.

+
-
-

Custom DICOM URL

- - -
+ -
-
Ready to load DICOM image
-
-
Click a sample URL above or enter a custom URL to load a DICOM image
-
-
+
+
+

About Us

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec ac orci vel nisi gravida feugiat.

+
- +
+

Our Services

+
    +
  • Service One
  • +
  • Service Two
  • +
  • Service Three
  • +
+
-
-
+
+

Contact

+
+
+

+ +
+

+ +
+

+ + +
+
+ - +
+

© 2025 Dummy Company. All rights reserved.

+
diff --git a/app/modules/Auth/redux/authActions.ts b/app/modules/Auth/redux/authActions.ts index d603953..5a5890c 100644 --- a/app/modules/Auth/redux/authActions.ts +++ b/app/modules/Auth/redux/authActions.ts @@ -18,7 +18,6 @@ export const login = createAsyncThunk( async (credentials: { email: string; password: string }, { rejectWithValue }) => { try { const response:any = await authAPI.login(credentials.email, credentials.password,'web'); - console.log('user response',response) if(response.data.message && !response.data.success){ showError(response.data.message) @@ -31,7 +30,7 @@ export const login = createAsyncThunk( if (response.ok && response.data && response.data.data) { // Return the user data for the fulfilled case - if(response.data.data.user.dashboard_role !=='radiologist'){ + if(response.data.data.user.dashboard_role !=='er_physician'){ showWarning('You are not authorized to access this application') return rejectWithValue('Not Authorized'); } diff --git a/app/modules/Auth/screens/LoginScreen.tsx b/app/modules/Auth/screens/LoginScreen.tsx index 259b9a5..bbb0f13 100644 --- a/app/modules/Auth/screens/LoginScreen.tsx +++ b/app/modules/Auth/screens/LoginScreen.tsx @@ -138,7 +138,7 @@ const LoginScreen: React.FC = ({ navigation }) => { * HEADER SECTION - App branding and title * ======================================================================== */} - Radiologist + Physician {/* Emergency Department Access */} diff --git a/app/modules/Auth/screens/SignUpScreen.tsx b/app/modules/Auth/screens/SignUpScreen.tsx index adfcd74..12e6416 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 = 'radiologist'; + let role = 'er_physician'; // Prepare form data with proper file handling const formFields = { diff --git a/app/modules/Dashboard/components/PredictionCard.tsx b/app/modules/Dashboard/components/PredictionCard.tsx new file mode 100644 index 0000000..a543a24 --- /dev/null +++ b/app/modules/Dashboard/components/PredictionCard.tsx @@ -0,0 +1,455 @@ +/* + * File: PredictionCard.tsx + * Description: Prediction card component for displaying AI prediction data and patient information + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + Image, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme/theme'; +import type { PredictionData } from '../types/predictions'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface PredictionCardProps { + prediction: PredictionData; + onPress: () => void; +} + +// ============================================================================ +// PREDICTION CARD COMPONENT +// ============================================================================ + +/** + * PredictionCard Component + * + * Purpose: Display AI prediction data with patient information in a card format + * + * Features: + * - Patient basic information + * - AI prediction results + * - Confidence scores + * - Clinical urgency + * - Feedback information + * - Modern card design + */ +export const PredictionCard: React.FC = ({ + prediction, + onPress, +}) => { + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + /** + * Get Urgency Color Configuration + * + * Purpose: Get color and icon based on clinical urgency + */ + const getUrgencyConfig = (urgency: string) => { + switch (urgency.toLowerCase()) { + case 'critical': + return { + color: theme.colors.error, + icon: 'alert-triangle', + bgColor: '#FFEBEE' + }; + case 'urgent': + return { + color: theme.colors.warning, + icon: 'clock', + bgColor: '#FFF3E0' + }; + case 'non-urgent': + return { + color: theme.colors.success, + icon: 'check-circle', + bgColor: '#E8F5E8' + }; + default: + return { + color: theme.colors.primary, + icon: 'info', + bgColor: '#E3F2FD' + }; + } + }; + + /** + * Get Confidence Color + * + * Purpose: Get color based on confidence score + */ + const getConfidenceColor = (confidence: number) => { + if (confidence >= 0.8) return theme.colors.success; + if (confidence >= 0.6) return theme.colors.warning; + return theme.colors.error; + }; + + /** + * Format Date + * + * Purpose: Format processed date for display + */ + const formatDate = (dateString: string) => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + const urgencyConfig = getUrgencyConfig(prediction.prediction.clinical_urgency); + const confidenceColor = getConfidenceColor(prediction.prediction.confidence_score); + + return ( + + {/* Header Section */} + + + + {prediction.patientdetails.Name || 'Unknown Patient'} + + + {prediction.patientdetails.PatID} • {prediction.patientdetails.PatAge} • {prediction.patientdetails.PatSex} + + + + {/* Urgency Badge */} + + + + {prediction.prediction.clinical_urgency} + + + + + {/* Prediction Results Section */} + + + + AI Prediction + + + + + Finding: + + {prediction.prediction.label} + + + + + Type: + + {prediction.prediction.finding_type} + + + + + Confidence: + + + {(prediction.prediction.confidence_score * 100).toFixed(1)}% + + + + + + + + {/* Medical Information Section */} + + + + + + {prediction.patientdetails.InstName || 'Unknown Institution'} + + + + + + + {prediction.patientdetails.Modality || 'Unknown Modality'} + + + + + + + + + {formatDate(prediction.processed_at)} + + + + + + + {prediction.prediction.processing_info.frame_count} Frames + + + + + + {/* Feedback Section */} + {prediction.has_provided_feedback && ( + + + + Feedback Available + + + + + {prediction.user_feedback_count} feedback(s) + + + Latest: {prediction.latest_feedback_type} + + + + )} + + {/* Footer */} + + + Processed: {formatDate(prediction.processed_at)} + + + + + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + borderRadius: theme.borderRadius.large, + padding: theme.spacing.md, + marginBottom: theme.spacing.md, + ...theme.shadows.primary, + }, + + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.md, + }, + + patientInfo: { + flex: 1, + marginRight: theme.spacing.sm, + }, + + patientName: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + patientDetails: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + urgencyBadge: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: theme.spacing.sm, + paddingVertical: theme.spacing.xs, + borderRadius: theme.borderRadius.small, + gap: theme.spacing.xs, + }, + + urgencyText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + textTransform: 'capitalize', + }, + + predictionSection: { + marginBottom: theme.spacing.md, + }, + + predictionHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.sm, + gap: theme.spacing.sm, + }, + + predictionTitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + }, + + predictionDetails: { + gap: theme.spacing.xs, + }, + + predictionRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + + predictionLabel: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + }, + + predictionValue: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + textTransform: 'capitalize', + }, + + confidenceContainer: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.xs, + }, + + confidenceValue: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.bold, + }, + + confidenceBar: { + width: 20, + height: 4, + borderRadius: 2, + }, + + medicalSection: { + marginBottom: theme.spacing.md, + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + + medicalRow: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: theme.spacing.sm, + }, + + medicalItem: { + flexDirection: 'row', + alignItems: 'center', + gap: theme.spacing.md, + flex: 1, + maxWidth: '48%', // Prevent items from taking too much space + }, + + medicalText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + flex: 1, // Allow text to take remaining space + height: 20, // Uniform height for all medical text + lineHeight: 20, // Ensure text is vertically centered + }, + + feedbackSection: { + marginBottom: theme.spacing.md, + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + + feedbackHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.xs, + gap: theme.spacing.xs, + }, + + feedbackTitle: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.success, + }, + + feedbackDetails: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + + feedbackCount: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + + feedbackType: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textPrimary, + textTransform: 'capitalize', + }, + + footer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingTop: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + + processedText: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textMuted, + }, + + actionButton: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: theme.colors.backgroundAlt, + justifyContent: 'center', + alignItems: 'center', + }, +}); + +/* + * End of File: PredictionCard.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/components/PredictionsList.tsx b/app/modules/Dashboard/components/PredictionsList.tsx new file mode 100644 index 0000000..b85c493 --- /dev/null +++ b/app/modules/Dashboard/components/PredictionsList.tsx @@ -0,0 +1,377 @@ +/* + * File: PredictionsList.tsx + * Description: Predictions list component with tabbed interface for feedback status + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + FlatList, + RefreshControl, +} from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; +import { theme } from '../../../theme/theme'; +import { PredictionCard } from './PredictionCard'; +import { usePredictions } from '../hooks/usePredictions'; +import type { PredictionData } from '../types/predictions'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface PredictionsListProps { + onPredictionPress: (prediction: PredictionData) => void; +} + +// ============================================================================ +// PREDICTIONS LIST COMPONENT +// ============================================================================ + +/** + * PredictionsList Component + * + * Purpose: Display AI predictions organized by radiologist feedback status + * + * Features: + * - Two tabs: Radiologist Reviewed and Pending Review + * - Search functionality + * - Pull-to-refresh + * - Loading states + * - Error handling + * - Empty states + */ +export const PredictionsList: React.FC = ({ + onPredictionPress, +}) => { + const { + activeTab, + currentPredictions, + currentLoadingState, + currentError, + switchTab, + refreshPredictions, + } = usePredictions(); + + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + /** + * Handle Tab Switch + * + * Purpose: Switch between radiologist feedback status tabs + */ + const handleTabSwitch = (tab: 'with-feedback' | 'without-feedback') => { + switchTab(tab); + }; + + /** + * Handle Prediction Press + * + * Purpose: Handle when a prediction card is pressed + */ + const handlePredictionPress = (prediction: PredictionData) => { + onPredictionPress(prediction); + }; + + /** + * Render Tab Button + * + * Purpose: Render individual radiologist feedback status tab button + */ + const renderTabButton = (tab: 'with-feedback' | 'without-feedback', label: string, icon: string) => { + const isActive = activeTab === tab; + + return ( + handleTabSwitch(tab)} + activeOpacity={0.7} + > + + + {label} + + + ); + }; + + /** + * Render Empty State + * + * Purpose: Render empty state when no predictions available + */ + const renderEmptyState = () => ( + + + No Predictions Found + + {activeTab === 'with-feedback' + ? 'No predictions have been reviewed by radiologists yet.' + : 'No predictions are waiting for radiologist review at the moment.' + } + + + ); + + /** + * Render Error State + * + * Purpose: Render error state when API call fails + */ + const renderErrorState = () => ( + + + Something went wrong + + {currentError || 'Failed to load predictions. Please try again.'} + + + + Retry + + + ); + + // ============================================================================ + // RENDER + // ============================================================================ + + // Debug logging to see current state + console.log('🔍 PredictionsList render debug:'); + console.log('Active tab:', activeTab); + console.log('Current predictions count:', currentPredictions.length); + console.log('Current predictions sample:', currentPredictions.slice(0, 2).map(p => ({ + id: p.id, + has_feedback: p.has_provided_feedback, + feedbacks_count: p.feedbacks?.length || 0 + }))); + + return ( + + {/* Tab Navigation */} + + {renderTabButton('with-feedback', 'Radiologist Reviewed', 'message-circle')} + {renderTabButton('without-feedback', 'Pending Review', 'message-square')} + + + {/* Content Area */} + + {currentError ? ( + renderErrorState() + ) : ( + <> + {/* Fixed Header - Not part of scrolling */} + {currentPredictions.length > 0 && ( + + + {activeTab === 'with-feedback' ? 'Radiologist Reviewed Predictions' : 'Predictions Awaiting Review'} + + + {currentPredictions.length} prediction{currentPredictions.length !== 1 ? 's' : ''} found + {activeTab === 'with-feedback' && ( + + {' • '}{currentPredictions.filter(p => p.feedbacks && p.feedbacks.length > 0).length} with feedback + + )} + + + )} + + {/* Horizontal Scrolling Predictions */} + ( + + handlePredictionPress(item)} + /> + + )} + keyExtractor={(item) => item.id.toString()} + contentContainerStyle={styles.listContainer} + showsHorizontalScrollIndicator={true} + showsVerticalScrollIndicator={false} + horizontal={true} + scrollEnabled={true} + refreshControl={ + + } + ListEmptyComponent={renderEmptyState} + /> + + )} + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + backgroundColor: theme.colors.background, + flex: 1, + }, + + tabContainer: { + flexDirection: 'row', + backgroundColor: theme.colors.background, + paddingHorizontal: theme.spacing.md, + paddingVertical: theme.spacing.sm, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + + tabButton: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.sm, + borderRadius: theme.borderRadius.medium, + gap: theme.spacing.xs, + }, + + activeTabButton: { + backgroundColor: theme.colors.tertiary, + }, + + tabButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textSecondary, + }, + + activeTabButtonText: { + color: theme.colors.primary, + }, + + contentContainer: { + flex: 1, + }, + + listContainer: { + padding: theme.spacing.md, + alignItems: 'flex-start', + }, + + listHeader: { + marginBottom: theme.spacing.md, + paddingBottom: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + width: '100%', + }, + + listHeaderTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginBottom: theme.spacing.xs, + }, + + listHeaderSubtitle: { + fontSize: theme.typography.fontSize.bodySmall, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + }, + feedbackCount: { + color: theme.colors.primary, + fontFamily: theme.typography.fontFamily.medium, + }, + + emptyState: { + justifyContent: 'center', + alignItems: 'center', + padding: theme.spacing.xxl, + }, + + emptyStateTitle: { + fontSize: theme.typography.fontSize.displayMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginTop: theme.spacing.md, + marginBottom: theme.spacing.sm, + textAlign: 'center', + }, + + emptyStateSubtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + }, + + errorState: { + justifyContent: 'center', + alignItems: 'center', + padding: theme.spacing.xxl, + }, + + errorStateTitle: { + fontSize: theme.typography.fontSize.displayMedium, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, + marginTop: theme.spacing.md, + marginBottom: theme.spacing.sm, + textAlign: 'center', + }, + + errorStateSubtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + textAlign: 'center', + marginBottom: theme.spacing.lg, + }, + + retryButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.primary, + paddingHorizontal: theme.spacing.lg, + paddingVertical: theme.spacing.md, + borderRadius: theme.borderRadius.medium, + gap: theme.spacing.sm, + }, + + retryButtonText: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.background, + }, + + predictionCardWrapper: { + marginRight: theme.spacing.md, + width: 280, // Uniform width for all cards + }, +}); + +/* + * End of File: PredictionsList.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/components/index.ts b/app/modules/Dashboard/components/index.ts index f893da3..d864ee4 100644 --- a/app/modules/Dashboard/components/index.ts +++ b/app/modules/Dashboard/components/index.ts @@ -4,4 +4,6 @@ export { DashboardHeader } from './DashboardHeader'; export { QuickActions } from './QuickActions'; export { DepartmentStats } from './DepartmentStats'; export { BrainPredictionsOverview } from './BrainPredictionsOverview'; -export { FeedbackAnalysisPieChart } from './FeedbackAnalysisPieChart'; \ No newline at end of file +export { FeedbackAnalysisPieChart } from './FeedbackAnalysisPieChart'; +export { PredictionCard } from './PredictionCard'; +export { PredictionsList } from './PredictionsList'; \ No newline at end of file diff --git a/app/modules/Dashboard/hooks/usePredictions.ts b/app/modules/Dashboard/hooks/usePredictions.ts new file mode 100644 index 0000000..3d1b12d --- /dev/null +++ b/app/modules/Dashboard/hooks/usePredictions.ts @@ -0,0 +1,182 @@ +/* + * File: usePredictions.ts + * Description: Custom hook for managing AI predictions state and actions + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { useEffect, useCallback } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import { + fetchAllPredictions, + setActiveTab, + setSearchQuery, + clearErrors, + clearSearch, + selectActiveTab, + selectSearchQuery, + selectPredictionsWithFeedback, + selectPredictionsWithoutFeedback, + selectIsLoading, + selectError, + selectCurrentPredictions, + selectCurrentLoadingState, + selectCurrentError, +} from '../redux/predictionsSlice'; +import { selectUser } from '../../Auth/redux'; +import type { PredictionTabType } from '../types/predictions'; + +// ============================================================================ +// USE PREDICTIONS HOOK +// ============================================================================ + +/** + * usePredictions Hook + * + * Purpose: Manage AI predictions state and provide actions + * + * Features: + * - Fetch all predictions from single API call + * - Frontend filtering for feedback status + * - Manage active tab state + * - Handle search functionality + * - Provide loading and error states + * - Auto-fetch data when needed + */ +export const usePredictions = () => { + const dispatch = useAppDispatch(); + + // ============================================================================ + // SELECTORS + // ============================================================================ + + const activeTab = useAppSelector(selectActiveTab); + const searchQuery = useAppSelector(selectSearchQuery); + + const predictionsWithFeedback = useAppSelector(selectPredictionsWithFeedback); + const predictionsWithoutFeedback = useAppSelector(selectPredictionsWithoutFeedback); + + const isLoading = useAppSelector(selectIsLoading); + const error = useAppSelector(selectError); + + const currentPredictions = useAppSelector(selectCurrentPredictions); + const currentLoadingState = useAppSelector(selectCurrentLoadingState); + const currentError = useAppSelector(selectCurrentError); + + // Get authentication token from auth store + const authToken = useAppSelector(selectUser)?.access_token; + + // ============================================================================ + // ACTIONS + // ============================================================================ + + /** + * Switch Active Tab + * + * Purpose: Change between feedback tabs + */ + const switchTab = useCallback((tab: PredictionTabType) => { + dispatch(setActiveTab(tab)); + }, [dispatch]); + + /** + * Update Search Query + * + * Purpose: Update search query for filtering predictions + */ + const updateSearchQuery = useCallback((query: string) => { + dispatch(setSearchQuery(query)); + }, [dispatch]); + + /** + * Clear Search + * + * Purpose: Clear search query + */ + const clearSearchQuery = useCallback(() => { + dispatch(clearSearch()); + }, [dispatch]); + + /** + * Clear Errors + * + * Purpose: Clear error states + */ + const clearErrorStates = useCallback(() => { + dispatch(clearErrors()); + }, [dispatch]); + + /** + * Refresh Predictions + * + * Purpose: Refresh all predictions data + */ + const refreshPredictions = useCallback(() => { + if (!authToken) return; + dispatch(fetchAllPredictions(authToken)); + }, [dispatch, authToken]); + + /** + * Fetch All Predictions + * + * Purpose: Fetch all predictions from API + */ + const fetchPredictions = useCallback(() => { + if (!authToken) return; + dispatch(fetchAllPredictions(authToken)); + }, [dispatch, authToken]); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * Auto-fetch data when component mounts or token changes + */ + useEffect(() => { + if (!authToken) return; + + // Only fetch if we don't have any predictions yet + if (predictionsWithFeedback.length === 0 && predictionsWithoutFeedback.length === 0) { + dispatch(fetchAllPredictions(authToken)); + } + }, [authToken, predictionsWithFeedback.length, predictionsWithoutFeedback.length, dispatch]); + + // ============================================================================ + // RETURN VALUES + // ============================================================================ + + return { + // State + activeTab, + searchQuery, + predictionsWithFeedback, + predictionsWithoutFeedback, + currentPredictions, + + // Loading states + isLoading, + currentLoadingState, + + // Error states + error, + currentError, + + // Actions + switchTab, + updateSearchQuery, + clearSearchQuery, + clearErrorStates, + refreshPredictions, + fetchPredictions, + + // Constants + authToken + }; +}; + +/* + * End of File: usePredictions.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/index.ts b/app/modules/Dashboard/index.ts index 0d0feb1..359a1b0 100644 --- a/app/modules/Dashboard/index.ts +++ b/app/modules/Dashboard/index.ts @@ -38,6 +38,8 @@ export { default as CriticalAlerts } from './components/CriticalAlerts'; export { default as DashboardHeader } from './components/DashboardHeader'; export { default as QuickActions } from './components/QuickActions'; export { default as DepartmentStats } from './components/DepartmentStats'; +export { PredictionCard } from './components/PredictionCard'; +export { PredictionsList } from './components/PredictionsList'; // Export hooks export * from './hooks'; @@ -84,6 +86,30 @@ export { selectTimeAnalysis, } from './redux/aiDashboardSelectors'; +// Export Predictions Redux +export { + fetchAllPredictions, + setActiveTab, + setSearchQuery, + clearErrors, + clearSearch, + filterPredictions, +} from './redux/predictionsSlice'; + +// Export Predictions Selectors +export { + selectActiveTab, + selectSearchQuery, + selectAllPredictions, + selectPredictionsWithFeedback, + selectPredictionsWithoutFeedback, + selectIsLoading, + selectError, + selectCurrentPredictions, + selectCurrentLoadingState, + selectCurrentError, +} from './redux/predictionsSlice'; + export { fetchAlerts, acknowledgeAlert, diff --git a/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx b/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx index ee262f1..be9c7fd 100644 --- a/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx +++ b/app/modules/Dashboard/navigation/DashboardStackNavigator.tsx @@ -11,9 +11,15 @@ import { createStackNavigator } from '@react-navigation/stack'; // Import dashboard screens import { DashboardScreen } from '../screens/DashboardScreen'; +// Import PatientCare screens for dashboard integration +// import { PatientDetailsScreen } from '../../PatientCare/screens/PatientDetailsScreen'; +// import { FeedbackDetailScreen } from '../../PatientCare/screens/FeedbackDetailScreen'; + // Import navigation types import { DashboardStackParamList } from './navigationTypes'; import { theme } from '../../../theme'; +import { PatientDetailsScreen } from '../../PatientCare'; +import FeedbackDetailScreen from '../../PatientCare/screens/FeedbackDetailScreen'; // Create stack navigator for Dashboard module const Stack = createStackNavigator(); @@ -78,6 +84,50 @@ const DashboardStackNavigator: React.FC = () => { headerShown: false, // Hide header for main dashboard }} /> + + {/* Patient Details Screen - Accessible from dashboard */} + + + {/* Feedback Detail Screen - Accessible from dashboard */} + ); }; diff --git a/app/modules/Dashboard/navigation/navigationTypes.ts b/app/modules/Dashboard/navigation/navigationTypes.ts index a0a8b5a..d84236c 100644 --- a/app/modules/Dashboard/navigation/navigationTypes.ts +++ b/app/modules/Dashboard/navigation/navigationTypes.ts @@ -21,6 +21,9 @@ export type DashboardStackParamList = { // Patient Details screen - Detailed patient information PatientDetails: PatientDetailsScreenParams; + // Feedback Detail screen - Series feedback information + FeedbackDetail: FeedbackDetailScreenParams; + // Alert Details screen - Detailed alert information AlertDetails: AlertDetailsScreenParams; @@ -88,6 +91,30 @@ export interface PatientDetailsScreenParams { fromScreen?: keyof DashboardStackParamList; } +/** + * FeedbackDetailScreenParams + * + * Purpose: Parameters for the feedback detail screen + * + * Parameters: + * - patientId: Required patient ID + * - patientName: Required patient name + * - seriesNumber: Required series number + * - seriesData: Optional series data + * - patientData: Optional patient data + * - feedbackData: Optional feedback data + * - onFeedbackSubmitted: Optional callback for feedback submission + */ +export interface FeedbackDetailScreenParams { + patientId: string; + patientName: string; + seriesNumber: string; + seriesData?: any; + patientData?: any; + feedbackData?: any[]; + onFeedbackSubmitted?: () => void; +} + /** * AlertDetailsScreenParams * @@ -149,6 +176,11 @@ export type DashboardScreenProps = DashboardScreenProps<'ERDashboard'>; */ export type PatientDetailsScreenProps = DashboardScreenProps<'PatientDetails'>; +/** + * FeedbackDetailScreenProps - Props for FeedbackDetailScreen component + */ +export type FeedbackDetailScreenProps = DashboardScreenProps<'FeedbackDetail'>; + /** * AlertDetailsScreenProps - Props for AlertDetailsScreen component */ diff --git a/app/modules/Dashboard/redux/predictionsSlice.ts b/app/modules/Dashboard/redux/predictionsSlice.ts new file mode 100644 index 0000000..44e0b94 --- /dev/null +++ b/app/modules/Dashboard/redux/predictionsSlice.ts @@ -0,0 +1,242 @@ +/* + * File: predictionsSlice.ts + * Description: Redux slice for managing AI predictions state + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; +import { predictionsAPI } from '../services/predictionsAPI'; +import type { PredictionsResponse, PredictionData, PredictionTabType } from '../types/predictions'; + +// ============================================================================ +// ASYNC THUNKS +// ============================================================================ + +/** + * Fetch All Predictions Async Thunk + * + * Purpose: Fetch all predictions and handle filtering on frontend + */ +export const fetchAllPredictions = createAsyncThunk( + 'predictions/fetchAll', + async (token: string, { rejectWithValue }) => { + try { + const response :any = await predictionsAPI.fetchAllPredictions(token); + console.log('dashboard predction data response', response); + if (response.ok && response.data && response.data.data) { + return response.data.data as PredictionData[]; + } else { + return rejectWithValue(response.problem || 'Failed to fetch predictions'); + } + } catch (error) { + return rejectWithValue('Network error occurred while fetching predictions'); + } + } +); + +// ============================================================================ +// STATE INTERFACE +// ============================================================================ + +interface PredictionsState { + // Data + allPredictions: PredictionData[]; + predictionsWithFeedback: PredictionData[]; + predictionsWithoutFeedback: PredictionData[]; + + // Loading states + isLoading: boolean; + + // Error states + error: string | null; + + // UI state + activeTab: PredictionTabType; + searchQuery: string; +} + +// ============================================================================ +// INITIAL STATE +// ============================================================================ + +const initialState: PredictionsState = { + // Data + allPredictions: [], + predictionsWithFeedback: [], + predictionsWithoutFeedback: [], + + // Loading states + isLoading: false, + + // Error states + error: null, + + // UI state + activeTab: 'with-feedback', + searchQuery: '', +}; + +// ============================================================================ +// PREDICTIONS SLICE +// ============================================================================ + +const predictionsSlice = createSlice({ + name: 'predictions', + initialState, + reducers: { + /** + * Set Active Tab + * + * Purpose: Switch between feedback tabs + */ + setActiveTab: (state, action: PayloadAction) => { + state.activeTab = action.payload; + }, + + /** + * Set Search Query + * + * Purpose: Update search query for filtering + */ + setSearchQuery: (state, action: PayloadAction) => { + state.searchQuery = action.payload; + }, + + /** + * Clear Errors + * + * Purpose: Clear error states + */ + clearErrors: (state) => { + state.error = null; + }, + + /** + * Clear Search + * + * Purpose: Clear search query + */ + clearSearch: (state) => { + state.searchQuery = ''; + }, + + /** + * Filter Predictions + * + * Purpose: Filter predictions based on feedback status + */ + filterPredictions: (state) => { + // Filter predictions with feedback + state.predictionsWithFeedback = state.allPredictions.filter( + prediction => prediction.has_provided_feedback + ); + + // Filter predictions without feedback + state.predictionsWithoutFeedback = state.allPredictions.filter( + prediction => !prediction.has_provided_feedback + ); + }, + }, + extraReducers: (builder) => { + // ============================================================================ + // FETCH ALL PREDICTIONS + // ============================================================================ + + // Pending + builder.addCase(fetchAllPredictions.pending, (state) => { + state.isLoading = true; + state.error = null; + }); + + // Fulfilled + builder.addCase(fetchAllPredictions.fulfilled, (state, action: PayloadAction) => { + state.isLoading = false; + state.allPredictions = action.payload; + state.error = null; + + // Debug logging to see what's happening with feedback filtering + console.log('🔍 Predictions filtering debug:'); + console.log('Total predictions:', action.payload.length); + console.log('Predictions with feedback field:', action.payload.filter(p => p.has_provided_feedback).length); + console.log('Predictions with feedbacks array:', action.payload.filter(p => p.feedbacks && p.feedbacks.length > 0).length); + console.log('Sample prediction feedback data:', action.payload.slice(0, 2).map(p => ({ + id: p.id, + has_provided_feedback: p.has_provided_feedback, + feedbacks_count: p.feedbacks?.length || 0, + user_feedback_count: p.user_feedback_count + }))); + + // Automatically filter predictions after fetching + // Primary filter: use has_provided_feedback field + // Fallback filter: check if feedbacks array has items + state.predictionsWithFeedback = action.payload.filter( + prediction => prediction.has_provided_feedback || (prediction.feedbacks && prediction.feedbacks.length > 0) + ); + state.predictionsWithoutFeedback = action.payload.filter( + prediction => !prediction.has_provided_feedback && (!prediction.feedbacks || prediction.feedbacks.length === 0) + ); + + console.log('Filtered results:'); + console.log('With feedback tab:', state.predictionsWithFeedback.length); + console.log('Without feedback tab:', state.predictionsWithoutFeedback.length); + }); + + // Rejected + builder.addCase(fetchAllPredictions.rejected, (state, action) => { + state.isLoading = false; + state.error = action.error.message || 'Failed to fetch predictions'; + }); + }, +}); + +// ============================================================================ +// ACTIONS +// ============================================================================ + +export const { + setActiveTab, + setSearchQuery, + clearErrors, + clearSearch, + filterPredictions, +} = predictionsSlice.actions; + +// ============================================================================ +// SELECTORS +// ============================================================================ + +export const selectActiveTab = (state: { predictions: PredictionsState }) => state.predictions.activeTab; +export const selectSearchQuery = (state: { predictions: PredictionsState }) => state.predictions.searchQuery; + +export const selectAllPredictions = (state: { predictions: PredictionsState }) => state.predictions.allPredictions; +export const selectPredictionsWithFeedback = (state: { predictions: PredictionsState }) => state.predictions.predictionsWithFeedback; +export const selectPredictionsWithoutFeedback = (state: { predictions: PredictionsState }) => state.predictions.predictionsWithoutFeedback; + +export const selectIsLoading = (state: { predictions: PredictionsState }) => state.predictions.isLoading; +export const selectError = (state: { predictions: PredictionsState }) => state.predictions.error; + +export const selectCurrentPredictions = (state: { predictions: PredictionsState }) => { + const { activeTab, predictionsWithFeedback, predictionsWithoutFeedback } = state.predictions; + return activeTab === 'with-feedback' ? predictionsWithFeedback : predictionsWithoutFeedback; +}; + +export const selectCurrentLoadingState = (state: { predictions: PredictionsState }) => { + return state.predictions.isLoading; +}; + +export const selectCurrentError = (state: { predictions: PredictionsState }) => { + return state.predictions.error; +}; + +// ============================================================================ +// EXPORT +// ============================================================================ + +export default predictionsSlice.reducer; + +/* + * End of File: predictionsSlice.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/screens/DashboardScreen.tsx b/app/modules/Dashboard/screens/DashboardScreen.tsx index 07b6c44..daffe32 100644 --- a/app/modules/Dashboard/screens/DashboardScreen.tsx +++ b/app/modules/Dashboard/screens/DashboardScreen.tsx @@ -5,7 +5,7 @@ * Copyright (c) Spurrin Innovations. All rights reserved. */ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { View, Text, @@ -15,12 +15,22 @@ import { RefreshControl, FlatList, Dimensions, + Alert, } from 'react-native'; +import Icon from 'react-native-vector-icons/Feather'; import { theme } from '../../../theme/theme'; import { DashboardHeader } from '../components/DashboardHeader'; import { BrainPredictionsOverview } from '../components/BrainPredictionsOverview'; import { FeedbackAnalysisPieChart } from '../components/FeedbackAnalysisPieChart'; +import { PredictionsList } from '../components/PredictionsList'; import { useAIDashboard } from '../hooks/useAIDashboard'; +import { selectUserDisplayName, selectUserFirstName } from '../../Auth/redux/authSelectors'; +import { useAppSelector } from '../../../store/hooks'; +import { CompositeNavigationProp } from '@react-navigation/native'; +import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs'; +import { StackNavigationProp } from '@react-navigation/stack'; +import { MainTabParamList } from '../../../navigation/navigationTypes'; +import { PatientCareStackParamList } from '../../PatientCare/navigation/navigationTypes'; /** * DashboardScreenProps Interface @@ -28,10 +38,15 @@ import { useAIDashboard } from '../hooks/useAIDashboard'; * Purpose: Defines the props required by the DashboardScreen component * * Props: - * - navigation: React Navigation object for screen navigation + * - navigation: Composite navigation object for tab and stack navigation */ +type DashboardScreenNavigationProp = CompositeNavigationProp< + BottomTabNavigationProp, + StackNavigationProp +>; + interface DashboardScreenProps { - navigation: any; + navigation: DashboardScreenNavigationProp; } /** @@ -123,7 +138,7 @@ export const DashboardScreen: React.FC = ({ navigation, }) => { // ============================================================================ - // CUSTOM HOOKS + // CUSTOM HOOKS & SELECTORS // ============================================================================ // Use custom hook for AI dashboard functionality @@ -136,6 +151,40 @@ export const DashboardScreen: React.FC = ({ refreshDashboardStatistics } = useAIDashboard(); + // Get user display name from auth state + const userDisplayName = useAppSelector(selectUserFirstName); + + // ============================================================================ + // HELPER FUNCTIONS + // ============================================================================ + + /** + * getPersonalizedGreeting Function + * + * Purpose: Generate a personalized greeting based on time of day and user's display name + * + * @returns Personalized greeting string + */ + const getPersonalizedGreeting = (): string => { + const currentHour = new Date().getHours(); + let timeGreeting = ''; + + // Determine time-based greeting + if (currentHour >= 5 && currentHour < 12) { + timeGreeting = 'Good Morning'; + } else if (currentHour >= 12 && currentHour < 17) { + timeGreeting = 'Good Afternoon'; + } else if (currentHour >= 17 && currentHour < 21) { + timeGreeting = 'Good Evening'; + } else { + timeGreeting = 'Good Evening'; + } + + // Create personalized greeting with fallback + const displayName = userDisplayName || 'Doctor'; + return `${timeGreeting}, Dr. ${displayName}`; + }; + // ============================================================================ // EVENT HANDLERS // ============================================================================ @@ -235,11 +284,20 @@ export const DashboardScreen: React.FC = ({ * @param value - Main value to display * @param subtitle - Optional subtitle * @param color - Optional color theme + * @param iconName - Icon name for the stats card * @returns Statistics card component */ - const renderStatsCard = (title: string, value: string | number, subtitle?: string, color?: string) => ( + const renderStatsCard = (title: string, value: string | number, subtitle?: string, color?: string, iconName?: string) => ( - {title} + {/* Icon and Title Row */} + + {iconName && ( + + + + )} + {title} + {value} {subtitle && {subtitle}} @@ -250,180 +308,12 @@ export const DashboardScreen: React.FC = ({ * * Purpose: Render confidence score breakdown section */ - const renderConfidenceBreakdown = () => { - // Check if dashboard data exists - if (!dashboardData) { - return ( - - Confidence Score Distribution - - Dashboard data not available - - Retry - - - - ); - } - // Check if confidence scores data exists - if (!dashboardData.data?.confidence_scores) { - return ( - - Confidence Score Distribution - - Confidence data not available - AI confidence scores are not currently accessible - - • AI system may be initializing - • Check system status - • Refresh in a few minutes - - - - ); - } - - const { high, medium, low } = dashboardData.data.confidence_scores; - - // Check if the object is empty or if all values are undefined/null/zero - if (!high && !medium && !low) { - return ( - - Confidence Score Distribution - - No data found - - - ); - } - - // Check if all required fields exist and are numbers - if (typeof high !== 'number' || typeof medium !== 'number' || typeof low !== 'number') { - return ( - - Confidence Score Distribution - - No confidence data available - - - ); - } - - const total = high + medium + low; - - // If no predictions, show empty state - if (total === 0) { - return ( - - Confidence Score Distribution - - No predictions available yet - AI predictions will appear here once the system processes medical scans - - - ); - } - - // Calculate percentages for better visualization - const highPercent = Math.round((high / total) * 100); - const mediumPercent = Math.round((medium / total) * 100); - const lowPercent = Math.round((low / total) * 100); - - // Helper function to get bar opacity - const getBarOpacity = (count: number) => { - if (count === 0) return 0.3; // Dimmed for zero values - return 0.9; // Full opacity for non-zero values - }; - - return ( - - Confidence Score Distribution - - {/* High Confidence */} - - - - High Confidence - {highPercent}% - - - - - {high} predictions - - - {/* Medium Confidence */} - - - - Medium Confidence - {mediumPercent}% - - - - - {medium} predictions - - - {/* Low Confidence */} - - - - Low Confidence - {lowPercent}% - - - - - {low} predictions - - - - {/* Summary Stats */} - - - Total Predictions: {total} - - - High Confidence Rate: {highPercent}% - - - - ); - }; /** * renderUrgencyBreakdown Function * - * Purpose: Render urgency level breakdown section + * Purpose: Render urgency level breakdown section with animated colored circles */ const renderUrgencyBreakdown = () => { if (!dashboardData?.data.urgency_levels) return null; @@ -457,21 +347,45 @@ export const DashboardScreen: React.FC = ({ return ( Case Urgency Distribution + - - + {/* Critical Cases Circle */} + + + {critical} + Critical - {critical} - - + + {/* Urgent Cases Circle */} + + + {urgent} + Urgent - {urgent} - - + + {/* Routine Cases Circle */} + + + {routine} + Routine - {routine} @@ -611,7 +525,7 @@ export const DashboardScreen: React.FC = ({ {/* Dashboard header with title and refresh button */} - AI Analysis Dashboard + {getPersonalizedGreeting()} {dashboardMessage} @@ -623,25 +537,29 @@ export const DashboardScreen: React.FC = ({ 'Total Predictions', dashboardData?.data.total_predictions || 0, 'AI analyses performed', - theme.colors.primary + theme.colors.primary, + 'activity' )} {renderStatsCard( 'Total Patients', dashboardData?.data.total_patients || 0, 'Unique patients', - theme.colors.info + theme.colors.info, + 'users' )} {renderStatsCard( 'Feedback Rate', `${dashboardData?.data.feedback_rate_percentage || 0}%`, 'User feedback coverage', - theme.colors.success + theme.colors.success, + 'message-circle' )} {renderStatsCard( 'Avg Confidence', (dashboardData?.data.average_confidence_score || 0).toFixed(2), 'AI prediction confidence', - theme.colors.warning + theme.colors.warning, + 'trending-up' )} @@ -693,8 +611,7 @@ export const DashboardScreen: React.FC = ({ {/* Dashboard header with key metrics */} {renderHeader()} - {/* Confidence score breakdown */} - {renderConfidenceBreakdown()} + {/* Urgency level breakdown */} {renderUrgencyBreakdown()} @@ -705,6 +622,85 @@ export const DashboardScreen: React.FC = ({ {/* Time-based analysis */} {renderTimeAnalysis()} + {/* AI Predictions List - Moved to main ScrollView */} + + AI Predictions + + Review AI predictions with and without user feedback + + + + {/* PredictionsList rendered directly */} + + { + try { + // Navigate to FeedbackDetailScreen with required parameters + + + navigation.navigate('Patients', { + screen: 'FeedbackDetail', + params: { + patientId: prediction.patid, + patientName: prediction.patientdetails.Name || 'Unknown Patient', + seriesNumber: prediction.prediction.processing_info.filename || 'Unknown Series', + seriesData: { + series_num: prediction.prediction.processing_info.filename || 'Unknown Series', + series_description: prediction.prediction.finding_type || 'AI Analysis', + total_images: prediction.prediction.processing_info.frame_count || 0, + png_preview: prediction.preview || '', + modality: prediction.patientdetails.Modality || 'Unknown' + }, + patientData: { + patid: prediction.patid, + hospital_id: prediction.hospital_id, + patient_info: { + name: prediction.patientdetails.Name || 'Unknown Patient', + age: prediction.patientdetails.PatAge || 'Unknown', + sex: prediction.patientdetails.PatSex || 'Unknown', + date: prediction.patientdetails.Date || 'Unknown', + institution: prediction.patientdetails.InstName || 'Unknown Institution', + modality: prediction.patientdetails.Modality || 'Unknown Modality', + status: prediction.patientdetails.Status || 'Unknown', + report_status: prediction.patientdetails.ReportStatus || 'Unknown', + file_name: prediction.prediction.processing_info.filename || 'Unknown', + file_type: prediction.prediction.processing_info.file_type || 'Unknown', + frame_count: prediction.prediction.processing_info.frame_count || 0 + }, + series_summary: [{ + series_num: prediction.prediction.processing_info.filename || 'Unknown Series', + series_description: prediction.prediction.finding_type || 'AI Analysis', + total_images: prediction.prediction.processing_info.frame_count || 0, + png_preview: prediction.preview || '', + modality: prediction.patientdetails.Modality || 'Unknown' + }], + processing_metadata: prediction.processing_metadata, + total_predictions: 1, + first_processed_at: prediction.processed_at, + last_processed_at: prediction.processed_at + }, + feedbackData: prediction.feedbacks || [], + onFeedbackSubmitted: () => { + // Refresh dashboard data when feedback is submitted + console.log('Feedback submitted, refreshing dashboard...'); + } + } + }); + + console.log('Navigation successful to FeedbackDetailScreen'); + } catch (error) { + console.error('Navigation error:', error); + // Fallback: show alert or handle error gracefully + Alert.alert( + 'Navigation Error', + 'Unable to open feedback details. Please try again.', + [{ text: 'OK' }] + ); + } + }} + /> + + {/* Bottom spacing for tab bar */} @@ -761,10 +757,11 @@ const styles = StyleSheet.create({ // Dashboard title styling dashboardTitle: { - fontSize: theme.typography.fontSize.displayLarge, + fontSize: theme.typography.fontSize.displayMedium, fontFamily: theme.typography.fontFamily.bold, color: theme.colors.textPrimary, marginBottom: theme.spacing.xs, + marginTop: theme.spacing.sm, }, // Dashboard subtitle styling @@ -794,12 +791,29 @@ const styles = StyleSheet.create({ ...theme.shadows.primary, }, + // Stats card header styling (icon + title row) + statsCardHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.xs, + gap: theme.spacing.sm, + }, + + // Stats card icon styling + statsCardIcon: { + width: 32, + height: 32, + borderRadius: theme.borderRadius.small, + justifyContent: 'center', + alignItems: 'center', + }, + // Stats card title styling statsCardTitle: { fontSize: theme.typography.fontSize.bodySmall, fontFamily: theme.typography.fontFamily.medium, color: theme.colors.textSecondary, - marginBottom: theme.spacing.xs, + flex: 1, }, // Stats card value styling @@ -834,6 +848,23 @@ const styles = StyleSheet.create({ color: theme.colors.textPrimary, marginBottom: theme.spacing.lg, }, + + // Section subtitle styling + sectionSubtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: theme.colors.textSecondary, + marginBottom: theme.spacing.md, + }, + + // Predictions container styling + predictionsContainer: { + height: 500, // Increased height for better FlatList performance + borderRadius: theme.borderRadius.medium, + overflow: 'hidden', + marginHorizontal: theme.spacing.md, + marginBottom: theme.spacing.md, + }, // Confidence breakdown container confidenceContainer: { @@ -910,37 +941,48 @@ const styles = StyleSheet.create({ urgencyContainer: { flexDirection: 'row', justifyContent: 'space-around', + alignItems: 'flex-start', + paddingHorizontal: theme.spacing.sm, }, - // Urgency item styling - urgencyItem: { + // Circle Container styling + circleContainer: { + width: 80, + height: 80, + borderRadius: 40, + justifyContent: 'center', alignItems: 'center', - flex: 1, + borderWidth: 8, + backgroundColor: theme.colors.background, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + elevation: 8, }, - - // Urgency indicator styling - urgencyIndicator: { - width: 16, - height: 16, - borderRadius: 8, - marginBottom: theme.spacing.xs, + + // Circle Value styling + circleValue: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.textPrimary, }, - - // Urgency label styling + + // Urgency Label styling urgencyLabel: { fontSize: theme.typography.fontSize.bodySmall, fontFamily: theme.typography.fontFamily.medium, color: theme.colors.textSecondary, - marginBottom: theme.spacing.xs, + marginTop: theme.spacing.sm, + textAlign: 'center', }, - - // Urgency value styling - urgencyValue: { - fontSize: theme.typography.fontSize.bodyMedium, - fontFamily: theme.typography.fontFamily.bold, - color: theme.colors.textPrimary, + + // Urgency Circle Item styling + urgencyCircleItem: { + alignItems: 'center', + flex: 1, }, - + // Feedback container styling feedbackContainer: { flexDirection: 'row', diff --git a/app/modules/Dashboard/services/predictionsAPI.ts b/app/modules/Dashboard/services/predictionsAPI.ts new file mode 100644 index 0000000..cbcb5e6 --- /dev/null +++ b/app/modules/Dashboard/services/predictionsAPI.ts @@ -0,0 +1,43 @@ +/* + * File: predictionsAPI.ts + * Description: API service for fetching AI prediction data and patient information + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import { create } from 'apisauce'; +import { API_CONFIG, buildHeaders } from '../../../shared/utils'; + +const api = create({ + baseURL: API_CONFIG.BASE_URL +}); + +/** + * PredictionsAPI Service + * + * Purpose: Handle API calls related to AI predictions and patient data + * + * Features: + * - Fetch all predictions from single endpoint + * - Frontend filtering for feedback status + * - Simple API calls without complex parameters + */ +export const predictionsAPI = { + /** + * Fetch All Predictions + * + * Purpose: Fetch all processed patient predictions + * + * @param token - Authentication token + * @returns Promise with all predictions response + */ + fetchAllPredictions: (token: string) => { + return api.get('/api/ai-cases/feedbacks/processed-patients', {}, buildHeaders({ token })); + } +}; + +/* + * End of File: predictionsAPI.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/Dashboard/types/predictions.ts b/app/modules/Dashboard/types/predictions.ts new file mode 100644 index 0000000..e7cced9 --- /dev/null +++ b/app/modules/Dashboard/types/predictions.ts @@ -0,0 +1,191 @@ +/* + * File: predictions.ts + * Description: Type definitions for AI prediction data and patient information + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +// ============================================================================ +// PREDICTION DATA TYPES +// ============================================================================ + +export interface PredictionProcessingInfo { + filename: string; + file_type: string; + frame_count: number; + is_multiframe: boolean; + averaging_applied: boolean; +} + +export interface StrokeDetection { + Normal: number; + Stroke: number; +} + +export interface BinaryHemorrhage { + Normal: number; + Hemorrhage: number; +} + +export interface HemorrhageDetection { + Epidural: number; + Subdural: number; + Subarachnoid: number; + "Midline shift": number; + Intraparenchymal: number; + Intraventricular: number; +} + +export interface DetailedResults { + stroke_detection: StrokeDetection; + binary_hemorrhage: BinaryHemorrhage; + hemorrhage_detection: HemorrhageDetection; +} + +export interface Prediction { + label: string; + finding_type: string; + processing_info: PredictionProcessingInfo; + clinical_urgency: string; + confidence_score: number; + detailed_results: DetailedResults; + finding_category: string; + primary_severity: string; + anatomical_location: string; +} + +export interface PatientDetails { + Date: string; + Name: string; + PatID: string; + PatAge: string; + PatSex: string; + Status: string; + InstName: string; + Modality: string; + ReportStatus: string | null; + medpacks_data: { + series: Array<{ + Path: string[]; + SerDes: string; + ViePos: string | null; + pngpath: string; + SeriesNum: string; + ImgTotalinSeries: string; + }>; + file_path: string; + basic_info: Record; + study_info: { + modality: string; + }; + hospital_id: string; + parsed_data: { + series: Array<{ + Path: string[]; + SerDes: string; + ViePos: string | null; + pngpath: string; + SeriesNum: string; + ImgTotalinSeries: string; + }>; + patientdetails: { + Date: string; + Name: string; + PatID: string; + PatAge: string; + PatSex: string; + Status: string; + InstName: string; + Modality: string; + ReportStatus: string | null; + }; + }; + dicom_images: any[]; + dicom_series: any[]; + dicom_studies: any[]; + image_details: Record; + all_dicom_data: any; + technical_details: Record; + patient_demographics: Record; + complete_patient_data: { + series: string; + file_id: string; + file_path: string; + series_id: string | null; + created_at: string; + updated_at: string; + hospital_id: string; + patientdetails: string; + }; + complete_dicom_details: any; + }; +} + +export interface ProcessingMetadata { + hospital_id: string; + processed_at: string; + ai_model_used: string; + original_patid: string; + sync_timestamp: string; + processed_file_path: string; + complete_dicom_fetched: boolean; +} + +export interface Feedback { + feedback_id: string; + patid: string; + prediction_id: number; + user_id: string; + feedback_text: string; + is_positive: boolean; + email: string; + created_at: string; + updated_at: string; +} + +export interface PredictionData { + id: number; + patid: string; + hospital_id: string; + prediction: Prediction; + patientdetails: PatientDetails; + processing_metadata: ProcessingMetadata; + file_path: string; + processed_at: string; + preview: string; + feedbacks: Feedback[]; + user_feedback_count: number; + latest_feedback_date: string; + latest_feedback_type: string; + has_provided_feedback: boolean; +} + +export interface PredictionsResponse { + success: boolean; + data: PredictionData[]; +} + +// ============================================================================ +// TAB TYPES +// ============================================================================ + +export type PredictionTabType = 'with-feedback' | 'without-feedback'; + +// ============================================================================ +// FILTER TYPES +// ============================================================================ + +export interface PredictionFilters { + urgency?: string; + finding_type?: string; + confidence_min?: number; + confidence_max?: number; + date_from?: string; + date_to?: string; +} + +/* + * End of File: predictions.ts + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/PatientCare/components/FilterTabs.tsx b/app/modules/PatientCare/components/FilterTabs.tsx index 7fe0143..8c5dd69 100644 --- a/app/modules/PatientCare/components/FilterTabs.tsx +++ b/app/modules/PatientCare/components/FilterTabs.tsx @@ -21,18 +21,17 @@ import Icon from 'react-native-vector-icons/Feather'; // ============================================================================ interface FilterTabsProps { - selectedFilter: 'all' | 'processed' | 'pending' | 'error'; - onFilterChange: (filter: 'all' | 'processed' | 'pending' | 'error') => void; + selectedFilter: 'all' | 'processed' | 'pending'; + onFilterChange: (filter: 'all' | 'processed' | 'pending') => void; patientCounts: { all: number; processed: number; pending: number; - error: number; }; } interface FilterTab { - id: 'all' | 'processed' | 'pending' | 'error'; + id: 'all' | 'processed' | 'pending'; label: string; icon: string; color: string; @@ -49,7 +48,7 @@ interface FilterTab { * Purpose: Provide filtering options for patient list * * Features: - * - Multiple filter options (All, Processed, Pending, Error) + * - Multiple filter options (All, Processed, Pending) * - Patient count display for each filter * - Visual indicators with icons and colors * - Horizontal scrollable layout @@ -87,13 +86,6 @@ const FilterTabs: React.FC = ({ color: theme.colors.warning, activeColor: theme.colors.warning, }, - { - id: 'error', - label: 'Error', - icon: 'alert-triangle', - color: theme.colors.error, - activeColor: theme.colors.error, - }, ]; // ============================================================================ @@ -116,8 +108,6 @@ const FilterTabs: React.FC = ({ return patientCounts.processed; case 'pending': return patientCounts.pending; - case 'error': - return patientCounts.error; default: return 0; } @@ -189,13 +179,6 @@ const FilterTabs: React.FC = ({ - - {/* Error Indicator */} - {tab.id === 'error' && patientCount > 0 && ( - - - - )} ); }; @@ -299,22 +282,6 @@ const styles = StyleSheet.create({ color: theme.colors.background, }, - // Error Indicator - errorIndicator: { - position: 'absolute', - top: 8, - right: 8, - width: 8, - height: 8, - }, - pulseDot: { - width: 8, - height: 8, - borderRadius: 4, - backgroundColor: theme.colors.error, - // Note: In a real app, you'd add animation here - }, - // Active Filter Indicator activeIndicator: { marginTop: theme.spacing.xs, diff --git a/app/modules/PatientCare/components/PatientCard.tsx b/app/modules/PatientCare/components/PatientCard.tsx index 8431994..d814637 100644 --- a/app/modules/PatientCare/components/PatientCard.tsx +++ b/app/modules/PatientCare/components/PatientCard.tsx @@ -1,6 +1,6 @@ /* * File: PatientCard.tsx - * Description: Patient card component for displaying DICOM medical case information + * Description: Enhanced patient card component for displaying DICOM medical case information * Design & Developed by Tech4Biz Solutions * Copyright (c) Spurrin Innovations. All rights reserved. */ @@ -33,16 +33,16 @@ interface PatientCardProps { /** * PatientCard Component * - * Purpose: Display DICOM medical case information in a card format + * Purpose: Display DICOM medical case information in a modern, enhanced card format * * Features: - * - Patient basic information from DICOM data - * - Modality and institution information - * - Processing status with color coding - * - Series information - * - Time since processed + * - Enhanced visual hierarchy with modern design + * - Improved status indicators and color coding + * - Better spacing and typography + * - Enhanced shadows and elevation + * - More intuitive information layout * - Emergency alert for critical cases - * - Modern ER-focused design + * - Modern ER-focused design with better UX */ const PatientCard: React.FC = ({ patient, @@ -56,36 +56,40 @@ const PatientCard: React.FC = ({ /** * Get Status Color Configuration * - * Purpose: Get color and icon based on processing status + * Purpose: Get enhanced color and icon based on processing status * * @param status - Processing status - * @returns Color configuration object + * @returns Enhanced color configuration object */ const getStatusConfig = (status: string) => { switch (status.toLowerCase()) { case 'processed': return { - color: theme.colors.success, + color: '#10B981', icon: 'check-circle', - bgColor: '#F0FFF4' + bgColor: '#ECFDF5', + borderColor: '#D1FAE5' }; case 'pending': return { - color: theme.colors.warning, + color: '#F59E0B', icon: 'clock', - bgColor: '#FFF8E1' + bgColor: '#FFFBEB', + borderColor: '#FEF3C7' }; case 'error': return { - color: theme.colors.error, + color: '#EF4444', icon: 'alert-triangle', - bgColor: '#FFF5F5' + bgColor: '#FEF2F2', + borderColor: '#FECACA' }; default: return { - color: theme.colors.primary, + color: '#3B82F6', icon: 'info', - bgColor: theme.colors.background + bgColor: '#EFF6FF', + borderColor: '#DBEAFE' }; } }; @@ -93,21 +97,21 @@ const PatientCard: React.FC = ({ /** * Get Modality Color * - * Purpose: Get color based on imaging modality + * Purpose: Get enhanced color based on imaging modality * * @param modality - Imaging modality - * @returns Color code + * @returns Enhanced color code */ const getModalityColor = (modality: string) => { switch (modality.toUpperCase()) { case 'CT': - return '#4A90E2'; + return '#3B82F6'; case 'MR': - return '#7B68EE'; + return '#8B5CF6'; case 'DX': - return '#50C878'; + return '#10B981'; case 'DICOM': - return '#FF6B6B'; + return '#EF4444'; default: return theme.colors.textSecondary; } @@ -164,21 +168,26 @@ const PatientCard: React.FC = ({ // ============================================================================ /** - * Render Status Badge + * Render Enhanced Status Badge * - * Purpose: Render processing status indicator badge + * Purpose: Render improved processing status indicator badge */ const renderStatusBadge = () => ( - - - {patientInfo.status} + + + + {patientInfo.status} + ); /** - * Render Emergency Button + * Render Enhanced Emergency Button * - * Purpose: Render emergency alert button for critical cases + * Purpose: Render improved emergency alert button for critical cases */ const renderEmergencyButton = () => { if (!isCritical) { @@ -189,18 +198,35 @@ const PatientCard: React.FC = ({ - + ALERT ); }; + /** + * Render Enhanced Modality Badge + * + * Purpose: Render improved modality indicator + */ + const renderModalityBadge = () => ( + + + {patientInfo.modality || 'N/A'} + + + ); + // ============================================================================ // MAIN RENDER // ============================================================================ - return ( = ({ { borderLeftColor: statusConfig.color } ]} onPress={onPress} - activeOpacity={0.7} + activeOpacity={0.8} > - {/* Header Section */} + {/* Enhanced Header Section */} - - {patientInfo.name || 'Unknown Patient'} - + + + {patientInfo.name || 'Unknown Patient'} + + {renderModalityBadge()} + ID: {patient.patid} • {patientInfo.age || 'N/A'}y • {patientInfo.sex || 'N/A'} @@ -227,72 +256,80 @@ const PatientCard: React.FC = ({ - {/* Medical Information Section */} + {/* Enhanced Medical Information Section */} - - - Modality - - {patientInfo.modality || 'N/A'} + + + + + {patient.total_files_processed || 0} - - Files - - {patient.total_files_processed} - - - Report - + + + + + {seriesCount} + + Series + + + + + {patientInfo.report_status || 'Pending'} + Report - {/* Institution */} + {/* Enhanced Institution Row */} - + + + {patientInfo.institution || 'Unknown Institution'} - {/* Series Information */} + {/* Enhanced Series Information */} - - Series Information + + Series Details + + + + {seriesCount} Series Available + + + {patientInfo.frame_count || 0} Total Frames + - - {seriesCount} Series Available • {patientInfo.frame_count} Total Frames - - {/* Footer */} + {/* Enhanced Footer */} {formatDate(patientInfo.date)} - {getTimeSinceProcessed(patient.last_processed_at)} + Processed {getTimeSinceProcessed(patient.last_processed_at)} Case #{patient.patid} - + + + @@ -300,21 +337,21 @@ const PatientCard: React.FC = ({ }; // ============================================================================ -// STYLES +// ENHANCED STYLES // ============================================================================ const styles = StyleSheet.create({ container: { backgroundColor: theme.colors.background, - borderRadius: 12, + borderRadius: 16, padding: theme.spacing.md, marginHorizontal: theme.spacing.md, marginVertical: theme.spacing.xs, - shadowColor: theme.colors.shadow, + shadowColor: '#000000', shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.1, - shadowRadius: 4, - elevation: 2, + shadowOpacity: 0.06, + shadowRadius: 8, + elevation: 3, borderWidth: 1, borderColor: theme.colors.border, borderLeftWidth: 4, @@ -322,10 +359,12 @@ const styles = StyleSheet.create({ containerCritical: { borderColor: theme.colors.error, borderWidth: 2, - backgroundColor: '#FFF5F5', + backgroundColor: '#FEF2F2', + shadowColor: theme.colors.error, + shadowOpacity: 0.15, }, - // Header Section + // Enhanced Header Section header: { flexDirection: 'row', justifyContent: 'space-between', @@ -334,156 +373,220 @@ const styles = StyleSheet.create({ }, headerLeft: { flex: 1, - marginRight: theme.spacing.sm, + marginRight: theme.spacing.md, }, headerRight: { flexDirection: 'row', alignItems: 'center', + gap: theme.spacing.sm, + }, + patientNameRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.xs, + gap: theme.spacing.xs, }, patientName: { - fontSize: 18, + fontSize: 20, color: theme.colors.textPrimary, fontFamily: theme.typography.fontFamily.bold, + flex: 1, }, patientInfo: { fontSize: 14, color: theme.colors.textSecondary, - marginTop: 2, - fontFamily: theme.typography.fontFamily.regular, + fontFamily: theme.typography.fontFamily.medium, + lineHeight: 20, }, - // Status Badge + // Enhanced Status Badge statusBadge: { flexDirection: 'row', alignItems: 'center', - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 12, - marginRight: theme.spacing.xs, - borderWidth: 1, + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 20, + borderWidth: 1.5, + gap: theme.spacing.xs, }, statusText: { - fontSize: 10, + fontSize: 11, fontFamily: theme.typography.fontFamily.bold, - marginLeft: 4, textTransform: 'uppercase', + letterSpacing: 0.5, }, - // Emergency Button + // Enhanced Emergency Button emergencyButton: { flexDirection: 'row', alignItems: 'center', backgroundColor: theme.colors.error, + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 20, + gap: theme.spacing.xs, + shadowColor: theme.colors.error, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.3, + shadowRadius: 4, + elevation: 3, + }, + emergencyButtonText: { + fontSize: 11, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + letterSpacing: 0.5, + }, + + // Enhanced Modality Badge + modalityBadge: { paddingHorizontal: 8, paddingVertical: 4, borderRadius: 12, + borderWidth: 1.5, }, - emergencyButtonText: { + modalityText: { fontSize: 10, fontFamily: theme.typography.fontFamily.bold, - color: theme.colors.background, - marginLeft: 4, + textTransform: 'uppercase', + letterSpacing: 0.5, }, - // Medical Section + // Enhanced Medical Section medicalSection: { marginBottom: theme.spacing.sm, paddingBottom: theme.spacing.sm, borderBottomWidth: 1, borderBottomColor: theme.colors.border, }, - infoRow: { + infoGrid: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: theme.spacing.sm, + gap: theme.spacing.sm, }, - infoItem: { + infoCard: { flex: 1, alignItems: 'center', + paddingVertical: theme.spacing.xs, + backgroundColor: theme.colors.backgroundAlt, + borderRadius: 12, + paddingHorizontal: theme.spacing.sm, + gap: theme.spacing.xs, }, infoLabel: { fontSize: 10, color: theme.colors.textMuted, - marginBottom: 2, textTransform: 'uppercase', - fontFamily: theme.typography.fontFamily.regular, + fontFamily: theme.typography.fontFamily.medium, + letterSpacing: 0.5, + textAlign: 'center', }, infoValue: { fontSize: 14, - fontFamily: theme.typography.fontFamily.bold, + fontFamily: theme.typography.fontFamily.medium, color: theme.colors.textPrimary, textAlign: 'center', }, - modalityText: { - fontFamily: theme.typography.fontFamily.bold, - }, - // Institution Row + // Enhanced Institution Row institutionRow: { flexDirection: 'row', alignItems: 'center', + backgroundColor: theme.colors.backgroundAlt, + padding: theme.spacing.sm, + borderRadius: 12, + gap: theme.spacing.sm, + }, + institutionIcon: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: theme.colors.primary + '20', + justifyContent: 'center', + alignItems: 'center', }, institutionText: { fontSize: 14, color: theme.colors.textSecondary, - marginLeft: 6, flex: 1, - fontFamily: theme.typography.fontFamily.regular, + fontFamily: theme.typography.fontFamily.medium, }, - // Series Section + // Enhanced Series Section seriesSection: { backgroundColor: theme.colors.backgroundAlt, - borderRadius: 8, + borderRadius: 12, padding: theme.spacing.sm, marginBottom: theme.spacing.sm, }, seriesHeader: { flexDirection: 'row', alignItems: 'center', - marginBottom: 4, + marginBottom: theme.spacing.sm, + gap: theme.spacing.xs, }, seriesLabel: { - fontSize: 12, - fontFamily: theme.typography.fontFamily.regular, + fontSize: 13, + fontFamily: theme.typography.fontFamily.medium, color: theme.colors.textSecondary, - marginLeft: 4, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + seriesInfo: { + gap: theme.spacing.xs, }, seriesText: { - fontSize: 14, + fontSize: 15, color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.medium, + }, + frameText: { + fontSize: 13, + color: theme.colors.textSecondary, fontFamily: theme.typography.fontFamily.regular, }, - // Footer Section + // Enhanced Footer Section footer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', + paddingTop: theme.spacing.sm, + borderTopWidth: 1, + borderTopColor: theme.colors.border, }, footerLeft: { flex: 1, }, dateText: { - fontSize: 12, + fontSize: 13, color: theme.colors.textMuted, - fontFamily: theme.typography.fontFamily.regular, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: 2, }, processedText: { - fontSize: 11, + fontSize: 12, color: theme.colors.textSecondary, - marginTop: 2, fontFamily: theme.typography.fontFamily.regular, }, footerRight: { flexDirection: 'row', alignItems: 'center', + gap: theme.spacing.sm, }, caseId: { fontSize: 12, color: theme.colors.textSecondary, - marginRight: theme.spacing.xs, - fontFamily: theme.typography.fontFamily.regular, + fontFamily: theme.typography.fontFamily.medium, + }, + chevronContainer: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: theme.colors.primary + '20', + justifyContent: 'center', + alignItems: 'center', }, }); diff --git a/app/modules/PatientCare/navigation/PatientCareStackNavigator.tsx b/app/modules/PatientCare/navigation/PatientCareStackNavigator.tsx index 6d7a68b..d2d0bf0 100644 --- a/app/modules/PatientCare/navigation/PatientCareStackNavigator.tsx +++ b/app/modules/PatientCare/navigation/PatientCareStackNavigator.tsx @@ -13,6 +13,7 @@ import { PatientsScreen, PatientDetailsScreen, SeriesDetailScreen } from '../scr // Import types import { PatientCareStackParamList } from './navigationTypes'; +import FeedbackDetailScreen from '../screens/FeedbackDetailScreen'; // ============================================================================ // STACK NAVIGATOR @@ -64,7 +65,7 @@ const PatientCareStackNavigator: React.FC = () => { gestureDirection: 'horizontal', }} /> - + {/* Series Detail Screen - Detailed series information with predictions and feedback */} { gestureDirection: 'horizontal', }} /> + + + ); }; diff --git a/app/modules/PatientCare/navigation/navigationTypes.ts b/app/modules/PatientCare/navigation/navigationTypes.ts index 4a92bef..96f9539 100644 --- a/app/modules/PatientCare/navigation/navigationTypes.ts +++ b/app/modules/PatientCare/navigation/navigationTypes.ts @@ -27,6 +27,9 @@ export type PatientCareStackParamList = { // Series Detail Screen - Detailed series information with predictions and feedback SeriesDetail: SeriesDetailScreenParams; + + // Feedback Detail Screen - Series feedback history and submission + FeedbackDetail: FeedbackDetailScreenParams; }; // ============================================================================ @@ -83,6 +86,32 @@ export interface SeriesDetailScreenParams { onFeedbackSubmitted?: () => void; } +/** + * FeedbackDetailScreenParams + * + * Purpose: Parameters for the feedback detail screen + * + * Parameters: + * - patientId: Required patient ID for the series + * - patientName: Required patient name for display + * - seriesNumber: Required series number to display + * - seriesData: Required series data object + * - patientData: Required patient data object for context + * - feedbackData: Required feedback data array for the series + * - onFeedbackSubmitted: Optional callback to refresh parent screen data + */ +export interface FeedbackDetailScreenParams { + patientId: string; + patientName: string; + seriesNumber: string; + seriesData: any; + patientData: any; + feedbackData: any[]; // Feedback data array for the series + // Callback function to refresh parent screen data when feedback is submitted + // This ensures PatientDetailsScreen shows updated information when user navigates back + onFeedbackSubmitted?: () => void; +} + // ============================================================================ // NAVIGATION PROP TYPES // ============================================================================ @@ -128,6 +157,16 @@ export interface SeriesDetailScreenProps { }; } +/** + * FeedbackDetailScreenProps - Props for FeedbackDetailScreen component + */ +export interface FeedbackDetailScreenProps { + navigation: PatientCareNavigationProp; + route: { + params: FeedbackDetailScreenParams; + }; +} + // ============================================================================ // NAVIGATION UTILITY TYPES // ============================================================================ diff --git a/app/modules/PatientCare/redux/patientCareSelectors.ts b/app/modules/PatientCare/redux/patientCareSelectors.ts index 4ecc20b..3eaf555 100644 --- a/app/modules/PatientCare/redux/patientCareSelectors.ts +++ b/app/modules/PatientCare/redux/patientCareSelectors.ts @@ -271,20 +271,17 @@ export const selectPatientStats = createSelector( total: 0, processed: 0, pending: 0, - error: 0, averageAge: 0, modalities: {}, totalFiles: 0, processedPercentage: 0, pendingPercentage: 0, - errorPercentage: 0, }; } const total = patients.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; // Calculate average age const totalAge = patients.reduce((sum: number, patient: PatientData) => { @@ -307,13 +304,11 @@ export const selectPatientStats = createSelector( total, processed, pending, - error, averageAge, 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, }; } ); @@ -392,14 +387,13 @@ export const selectPatientCounts = createSelector( [selectPatients], (patients) => { if (!patients || !Array.isArray(patients)) { - return { all: 0, processed: 0, pending: 0, error: 0 }; + return { all: 0, processed: 0, pending: 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 8222b65..a81a7d3 100644 --- a/app/modules/PatientCare/redux/patientCareSlice.ts +++ b/app/modules/PatientCare/redux/patientCareSlice.ts @@ -63,7 +63,7 @@ export interface PatientCareState { // Search and filtering searchQuery: string; - selectedFilter: 'all' | 'processed' | 'pending' | 'error'; + selectedFilter: 'all' | 'processed' | 'pending'; sortBy: 'date' | 'name' | 'processed'; sortOrder: 'asc' | 'desc'; @@ -93,7 +93,6 @@ export const fetchPatients = createAsyncThunk( async (token: string, { rejectWithValue }) => { try { 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 @@ -338,7 +337,7 @@ const patientCareSlice = createSlice({ * * Purpose: Set patient filter */ - setFilter: (state, action: PayloadAction<'all' | 'processed' | 'pending' | 'error'>) => { + setFilter: (state, action: PayloadAction<'all' | 'processed' | 'pending'>) => { state.selectedFilter = action.payload; state.currentPage = 1; // Reset to first page when filtering }, diff --git a/app/modules/PatientCare/screens/FeedbackDetailScreen.tsx b/app/modules/PatientCare/screens/FeedbackDetailScreen.tsx new file mode 100644 index 0000000..de1453d --- /dev/null +++ b/app/modules/PatientCare/screens/FeedbackDetailScreen.tsx @@ -0,0 +1,634 @@ +/* + * File: FeedbackDetailScreen.tsx + * Description: Feedback detail screen for a specific series showing feedback history (read-only) + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + * + * Features: + * - Display feedback history for the series (read-only) + * - Feedback data received from navigation parameters + * - Clinical insights and feedback analytics + * - Modern healthcare-focused UI design + */ + +import React, { useEffect, useState, useCallback } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + StatusBar, + Alert, + TextInput, + RefreshControl, +} from 'react-native'; +import { theme } from '../../../theme/theme'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import Icon from 'react-native-vector-icons/Feather'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +// Import types and API +import { selectUser } from '../../Auth/redux/authSelectors'; +import { FeedbackDetailScreenProps } from '../../Dashboard/navigation/navigationTypes'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +interface SeriesSummary { + series_num: string; + series_description: string; + total_images: number; + png_preview: string; + modality: string; +} + +interface Feedback { + feedback_id: string; + user_id: string; + feedback_text: string; + is_positive: boolean; + email: string; + created_at: string; + prediction_id: number; + prediction_file_path: string; + series_number: string; + feedback_type: string; +} + +interface PatientData { + patid: string; + hospital_id: string; + patient_info: { + 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; + }; + series_summary: SeriesSummary[]; + processing_metadata: any; + total_predictions: number; + first_processed_at: string; + last_processed_at: string; +} + +// ============================================================================ +// FEEDBACK DETAIL SCREEN COMPONENT +// ============================================================================ + +/** + * FeedbackDetailScreen Component + * + * Purpose: Display feedback details and history for a specific series (read-only) + * + * Features: + * - Feedback history display (read-only) + * - Clinical insights and analytics + * - Modern healthcare-focused UI design + */ +const FeedbackDetailScreen: React.FC = ({ navigation, route }) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const dispatch = useAppDispatch(); + + // Route parameters + const { patientId, patientName, seriesNumber, seriesData, patientData, feedbackData, onFeedbackSubmitted } = route.params; + + // Redux state + const user = useAppSelector(selectUser); + + // Local state + const [isLoading, setIsLoading] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); + const [error, setError] = useState(null); + + // ============================================================================ + // EFFECTS + // ============================================================================ + + /** + * Component Mount Effect + * + * Purpose: Initialize screen and set navigation title + */ + useEffect(() => { + navigation.setOptions({ + title: `Feedback - Series ${seriesNumber}`, + headerShown: true, + headerLeft: () => ( + + + + ), + }); + }, [navigation, seriesNumber, patientId]); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle Back Navigation + * + * Purpose: Navigate back to previous screen + */ + const handleBackPress = useCallback(() => { + navigation.goBack(); + }, [navigation]); + + /** + * Handle Refresh + * + * Purpose: Pull-to-refresh functionality + */ + const handleRefresh = useCallback(async () => { + setIsRefreshing(true); + // TODO: Implement actual refresh logic + setTimeout(() => { + setIsRefreshing(false); + }, 1000); + }, []); + + /** + * Handle Back to Patient Details + * + * Purpose: Navigate to PatientDetails screen within the Dashboard stack + * + * Note: Now that both screens are in the same Dashboard stack, + * navigation should work smoothly without loops. + */ + const handleBackToPatientDetails = useCallback(() => { + try { + // Navigate to PatientDetails screen in the same stack + navigation.navigate('PatientDetails', { + patientId: patientId, + patient: patientData || { name: patientName || 'Unknown Patient' }, + }); + } catch (error) { + console.warn('Navigation to PatientDetails failed:', error); + // Fallback: go back to previous screen + navigation.goBack(); + } + }, [navigation, patientId, patientName, patientData]); + + // ============================================================================ + // UTILITY FUNCTIONS + // ============================================================================ + + /** + * Get Feedback Type Color + * + * Purpose: Get appropriate color for feedback type + * + * @param feedbackType - Type of feedback + */ + const getFeedbackTypeColor = (feedbackType: string) => { + switch (feedbackType.toLowerCase()) { + case 'clinical_accuracy': + return theme.colors.success; + case 'confidence_assessment': + return theme.colors.warning; + case 'technical_issue': + return theme.colors.error; + default: + return theme.colors.info; + } + }; + + /** + * Get Series Feedback + * + * Purpose: Get feedback for the current series + */ + const getSeriesFeedback = () => { + return feedbackData?.filter((feedback: Feedback) => feedback.series_number === seriesNumber) || []; + }; + + /** + * Is Feedback New + * + * Purpose: Check if feedback is recent (within 24 hours) + * + * @param feedbackId - Feedback ID to check + */ + const isFeedbackNew = (feedbackId: string) => { + const feedback = feedbackData?.find((f: Feedback) => f.feedback_id === feedbackId); + if (!feedback) return false; + + const feedbackDate = new Date(feedback.created_at); + const now = new Date(); + const diffHours = (now.getTime() - feedbackDate.getTime()) / (1000 * 60 * 60); + + return diffHours < 24; + }; + + // ============================================================================ + // RENDER HELPERS + // ============================================================================ + + /** + * Render Series Header + * + * Purpose: Render series information header + */ + const renderSeriesHeader = () => { + if (!seriesData) return null; + + return ( + + + + + Series {seriesData.series_description} + + + {seriesData.total_images} images • {seriesData.modality} modality + + + + + + Processed + + + + + {patientData && ( + + + + {patientData.patient_info.name} • ID: {patientData.patid} + + + + + View Details + + + + )} + + ); + }; + + /** + * Render Feedback History + * + * Purpose: Render feedback history display only + */ + const renderFeedbackHistory = () => { + const seriesFeedbacks = getSeriesFeedback(); + + return ( + + {/* Feedback History */} + + + Feedback History ({seriesFeedbacks.length}) + + + {seriesFeedbacks.length === 0 ? ( + + + No Feedback Yet + + No feedback has been provided for this series yet + + + ) : ( + seriesFeedbacks.map((feedback: Feedback) => ( + + + + + + + {feedback.email} + {isFeedbackNew(feedback.feedback_id) && ( + + NEW + + )} + + + {new Date(feedback.created_at).toLocaleDateString()} + + + + {feedback.feedback_text} + + + + + {feedback.feedback_type.replace('_', ' ').toUpperCase()} + + + + + )) + )} + + + ); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + + + {/* Fixed Series Header */} + {renderSeriesHeader()} + + + + {/* Scrollable Feedback Content */} + + + } + > + {/* Feedback History from Navigation Parameters */} + {renderFeedbackHistory()} + + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + + // Series Header Styles + seriesHeader: { + backgroundColor: theme.colors.background, + padding: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + // Fixed Feedback Title Styles + fixedFeedbackTitle: { + backgroundColor: theme.colors.background, + padding: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + seriesHeaderTop: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.sm, + }, + seriesHeaderLeft: { + flex: 1, + marginRight: theme.spacing.md, + }, + seriesHeaderRight: { + alignItems: 'flex-end', + }, + seriesTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: 4, + }, + seriesMeta: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + seriesStatusBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.success, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + seriesStatusText: { + fontSize: 10, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: 4, + textTransform: 'uppercase', + }, + patientInfoRow: { + flexDirection: 'row', + alignItems: 'center', + }, + patientInfoText: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + marginLeft: theme.spacing.xs, + flex: 1, + }, + patientDetailButton: { + padding: theme.spacing.xs, + marginLeft: theme.spacing.sm, + backgroundColor: theme.colors.tertiary, + borderRadius: 16, + borderWidth: 1, + borderColor: theme.colors.primary, + }, + patientDetailButtonContent: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 8, + paddingVertical: 4, + }, + patientDetailButtonText: { + fontSize: 11, + color: theme.colors.primary, + fontFamily: theme.typography.fontFamily.medium, + marginLeft: 4, + textTransform: 'uppercase', + }, + + // Content Styles + scrollableContent: { + flex: 1, + }, + content: { + flex: 1, + }, + + // Section Styles + sectionTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.md, + }, + + // Feedback Styles + feedbackHistory: { + padding: theme.spacing.md, + }, + + // Feedback List Styles + feedbackList: { + marginTop: theme.spacing.sm, + }, + feedbackListTitle: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.md, + }, + feedbackCard: { + 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, + }, + feedbackCardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + feedbackCardHeaderLeft: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + feedbackTypeIndicator: { + width: 20, + height: 20, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + marginRight: theme.spacing.sm, + }, + feedbackEmail: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginRight: theme.spacing.sm, + }, + newFeedbackBadge: { + backgroundColor: theme.colors.error, + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 8, + }, + newFeedbackBadgeText: { + fontSize: 8, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + textTransform: 'uppercase', + }, + feedbackTimestamp: { + fontSize: 10, + color: theme.colors.textMuted, + fontFamily: theme.typography.fontFamily.regular, + }, + feedbackText: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + lineHeight: 20, + marginBottom: theme.spacing.sm, + }, + feedbackCardFooter: { + alignItems: 'flex-end', + }, + feedbackTypeBadge: { + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + }, + feedbackTypeBadgeText: { + fontSize: 10, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + textTransform: 'uppercase', + }, + + // Empty State Styles + emptyFeedbackState: { + alignItems: 'center', + justifyContent: 'center', + paddingVertical: theme.spacing.xl, + }, + emptyFeedbackTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginTop: theme.spacing.md, + marginBottom: theme.spacing.sm, + }, + emptyFeedbackSubtitle: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'center', + lineHeight: 20, + }, + + // Header Back Button Style + headerBackButton: { + padding: theme.spacing.sm, + marginLeft: theme.spacing.xs, + }, +}); + +export default FeedbackDetailScreen; + +/* + * End of File: FeedbackDetailScreen.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/modules/PatientCare/screens/PatientDetailsScreen.tsx b/app/modules/PatientCare/screens/PatientDetailsScreen.tsx index bc937c1..9476a1f 100644 --- a/app/modules/PatientCare/screens/PatientDetailsScreen.tsx +++ b/app/modules/PatientCare/screens/PatientDetailsScreen.tsx @@ -32,6 +32,7 @@ import { theme } from '../../../theme/theme'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import Icon from 'react-native-vector-icons/Feather'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { DicomViewerModal } from '../../../shared/components'; // Import types and API import { patientAPI } from '../services/patientAPI'; @@ -97,6 +98,7 @@ interface PatientData { first_processed_at: string; last_processed_at: string; predictions_by_series: { [key: string]: Prediction[] }; + feedback_by_series: { [key: string]: any[] }; // Add feedback data by series } // ============================================================================ @@ -140,10 +142,24 @@ const PatientDetailsScreen: React.FC = ({ navigation, const [error, setError] = useState(null); const [selectedImageIndex, setSelectedImageIndex] = useState(0); const [showFullImage, setShowFullImage] = useState(false); - const [activeTab, setActiveTab] = useState<'overview' | 'aiAnalysis' | 'history'>('overview'); + const [activeTab, setActiveTab] = useState<'overview' | 'aiAnalysis' | 'feedbacks'>('overview'); - // Navigation state - const [selectedSeriesForDetail, setSelectedSeriesForDetail] = useState(null); + // DICOM Modal state + const [dicomModalVisible, setDicomModalVisible] = useState(false); + const [selectedDicomData, setSelectedDicomData] = useState<{ + dicomUrl: string; + seriesData: SeriesSummary; + prediction?: Prediction; + } | null>(null); + + + + // Additional Analysis Modal state + const [analysisModalVisible, setAnalysisModalVisible] = useState(false); + const [selectedSeriesForAnalysis, setSelectedSeriesForAnalysis] = useState<{ + series: SeriesSummary; + predictions: Prediction[]; + } | null>(null); // ============================================================================ // DATA FETCHING @@ -192,6 +208,23 @@ const PatientDetailsScreen: React.FC = ({ navigation, fetchPatientData(); }, [fetchPatientData]); + /** + * Route Parameters Change Effect + * + * Purpose: Fetch fresh patient data whenever route parameters change + * This ensures that when navigating from different screens with different patient IDs, + * the screen always loads the correct patient data + */ + useEffect(() => { + // Reset state when patientId changes + setPatientData(null); + setError(null); + setIsLoading(true); + + // Fetch new patient data + fetchPatientData(); + }, [patientId]); // Only depend on patientId, not the entire fetchPatientData function + /** * Navigation Title Effect * @@ -222,24 +255,77 @@ const PatientDetailsScreen: React.FC = ({ navigation, }, [fetchPatientData]); /** - * Handle Image Press + * Handle DICOM Image Press * - * Purpose: Open full-screen image viewer for selected series + * Purpose: Open DICOM viewer modal for selected series * * @param seriesIndex - Index of the series */ const handleImagePress = useCallback((seriesIndex: number) => { - setSelectedImageIndex(seriesIndex); - setShowFullImage(true); - }, []); + if (!patientData || !patientData.series_summary[seriesIndex]) return; + + const series = patientData.series_summary[seriesIndex]; + const seriesPredictions = patientData.predictions_by_series[series.series_num] || []; + const firstPrediction = seriesPredictions[0]; + + if (firstPrediction?.preview) { + const dicomUrl = API_CONFIG.BASE_URL +'/api/dicom'+ firstPrediction.file_path; + console.log('dicomUrl', dicomUrl); + setSelectedDicomData({ + dicomUrl, + seriesData: series, + prediction: firstPrediction, + }); + setDicomModalVisible(true); + } else { + Alert.alert( + 'No DICOM Available', + 'No DICOM image is available for this series.', + [{ text: 'OK' }] + ); + } + }, [patientData]); /** - * Handle Close Image Viewer + * Handle Open DICOM Modal * - * Purpose: Close full-screen image viewer + * Purpose: Open DICOM modal with specific series and prediction data + * + * @param series - Series data + * @param prediction - Optional prediction data */ - const handleCloseImageViewer = useCallback(() => { - setShowFullImage(false); + const handleOpenDicomModal = useCallback((series: SeriesSummary, prediction?: Prediction) => { + if (!patientData) return; + + const seriesPredictions = patientData.predictions_by_series[series.series_num] || []; + const targetPrediction = prediction || seriesPredictions[0]; + + if (targetPrediction?.preview) { + const dicomUrl = API_CONFIG.DICOM_BASE_URL + targetPrediction.preview; + + setSelectedDicomData({ + dicomUrl, + seriesData: series, + prediction: targetPrediction, + }); + setDicomModalVisible(true); + } else { + Alert.alert( + 'No DICOM Available', + 'No DICOM image is available for this series.', + [{ text: 'OK' }] + ); + } + }, [patientData]); + + /** + * Handle Close DICOM Modal + * + * Purpose: Close DICOM viewer modal and reset state + */ + const handleCloseDicomModal = useCallback(() => { + setDicomModalVisible(false); + setSelectedDicomData(null); }, []); /** @@ -331,22 +417,54 @@ const PatientDetailsScreen: React.FC = ({ navigation, // NAVIGATION HANDLERS // ============================================================================ + + /** - * Handle Navigate to Series Detail + * Handle Open Additional Analysis Modal * - * Purpose: Navigate to detailed series view + * Purpose: Open modal to show detailed analysis results for a series * - * @param series - Series data to view in detail + * @param series - Series data to show analysis for */ - const handleNavigateToSeriesDetail = useCallback((series: SeriesSummary) => { + const handleOpenAnalysisModal = useCallback((series: SeriesSummary) => { if (!patientData) return; - navigation.navigate('SeriesDetail', { + const seriesPredictions = patientData.predictions_by_series[series.series_num] || []; + + setSelectedSeriesForAnalysis({ + series, + predictions: seriesPredictions + }); + setAnalysisModalVisible(true); + }, [patientData]); + + /** + * Handle Close Additional Analysis Modal + * + * Purpose: Close the additional analysis modal and reset state + */ + const handleCloseAnalysisModal = useCallback(() => { + setAnalysisModalVisible(false); + setSelectedSeriesForAnalysis(null); + }, []); + + /** + * Handle Navigate to Feedback Detail + * + * Purpose: Navigate to feedback detail screen for a specific series + * + * @param series - Series data to view feedback for + */ + const handleNavigateToFeedbackDetail = useCallback((series: SeriesSummary) => { + if (!patientData) return; + + navigation.navigate('FeedbackDetail', { patientId: patientData.patid, patientName: patientData.patient_info.name, seriesNumber: series.series_num, seriesData: series, patientData: patientData, + feedbackData: patientData.feedback_by_series[series.series_num], // Initialize with empty array for new feedback // Pass the refresh function as callback so parent screen can update when feedback is submitted onFeedbackSubmitted: fetchPatientData }); @@ -396,6 +514,133 @@ const PatientDetailsScreen: React.FC = ({ navigation, } }; + /** + * Get Percentage Color + * + * Purpose: Get appropriate color based on percentage value + * + * @param percentage - Percentage value (0-100) + */ + const getPercentageColor = (percentage: number) => { + if (percentage >= 70) { + return theme.colors.error; // High detection - Red + } else if (percentage >= 40) { + return theme.colors.warning; // Medium detection - Orange + } else if (percentage >= 10) { + return theme.colors.info; // Low detection - Blue + } else { + return theme.colors.success; // No detection - Green + } + }; + + /** + * Get Percentage Value + * + * Purpose: Extract percentage value from prediction data with flexible field naming + * + * @param prediction - Prediction object + * @param type - Hemorrhage type (epidural, subdural, etc.) + */ + const getPercentageValue = (prediction: any, type: string): number => { + // First, try to get from detailed_results.hemorrhage_detection + if (prediction.detailed_results?.hemorrhage_detection) { + const hemorrhageData = prediction.detailed_results.hemorrhage_detection; + + // Map our types to the actual field names in the data + const fieldMapping: { [key: string]: string } = { + 'epidural': 'Epidural', + 'subdural': 'Subdural', + 'subarachnoid': 'Subarachnoid', + 'intraparenchymal': 'Intraparenchymal', + 'intraventricular': 'Intraventricular', + 'midline_shift': 'Midline shift' + }; + + const actualFieldName = fieldMapping[type]; + if (actualFieldName && hemorrhageData[actualFieldName] !== undefined) { + const value = hemorrhageData[actualFieldName]; + + // Convert decimal (0-1) to percentage (0-100) + if (typeof value === 'number' && value >= 0 && value <= 1) { + return value * 100; + } + + // If it's already a percentage, return as is + if (typeof value === 'number' && value >= 0 && value <= 100) { + return value; + } + + // Handle string values + if (typeof value === 'string') { + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + if (numValue >= 0 && numValue <= 1) { + return numValue * 100; + } + if (numValue >= 0 && numValue <= 100) { + return numValue; + } + } + } + } + } + + // Fallback: try different possible field names for the percentage + const possibleFields = [ + `${type}_percentage`, + `${type}_score`, + `${type}_value`, + `${type}_detection`, + `${type}_probability`, + type + ]; + + for (const field of possibleFields) { + if (prediction[field] !== undefined && prediction[field] !== null) { + const value = prediction[field]; + + // Handle different data types + if (typeof value === 'number') { + // If it's already a percentage (0-100), return as is + if (value >= 0 && value <= 100) { + return value; + } + // If it's a decimal (0-1), convert to percentage + if (value >= 0 && value <= 1) { + return value * 100; + } + // If it's a large number, assume it's already a percentage + return value; + } + + // Handle string values + if (typeof value === 'string') { + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + // If it's already a percentage (0-100), return as is + if (numValue >= 0 && numValue <= 100) { + return numValue; + } + // If it's a decimal (0-1), convert to percentage + if (numValue >= 0 && numValue <= 1) { + return numValue * 100; + } + // If it's a large number, assume it's already a percentage + return numValue; + } + } + + // Handle boolean values (convert to 0% or 100%) + if (typeof value === 'boolean') { + return value ? 100 : 0; + } + } + } + + // If no valid percentage found, return 0 + return 0; + }; + // ============================================================================ // RENDER HELPERS // ============================================================================ @@ -501,7 +746,7 @@ const PatientDetailsScreen: React.FC = ({ navigation, {[ { key: 'overview', label: 'Overview', icon: 'info' }, { key: 'aiAnalysis', label: 'AI Analysis', icon: 'activity', count: patientData.series_summary.length }, - { key: 'history', label: 'History', icon: 'clock' }, + { key: 'feedbacks', label: 'Feedbacks', icon: 'message-circle', count: patientData.total_predictions }, ].map((tab) => ( = ({ navigation, const hasPredictions = seriesPredictions.length > 0; return ( - + {/* Series Header */} Series {series.series_num}: {series.series_description} - handleNavigateToSeriesDetail(series)} - activeOpacity={0.7} - > - - Series Details + handleOpenAnalysisModal(series)} + activeOpacity={0.7} + > + + Additional Analysis @@ -784,7 +1029,7 @@ const PatientDetailsScreen: React.FC = ({ navigation, {hasPredictions ? ( seriesPredictions.map((prediction) => ( - + {prediction.prediction.label} = ({ navigation, }; /** - * Render History Tab + * Render Feedbacks Tab * - * Purpose: Render patient medical history + * Purpose: Render series feedback list with navigation to feedback details */ - const renderHistoryTab = () => { + const renderFeedbacksTab = () => { if (!patientData) return null; + if (patientData.series_summary.length === 0) { + return ( + + + + No Series Available + + No DICOM series are currently available for this patient + + + + ); + } + return ( - - Processing History - - - - First processed on {new Date(patientData.first_processed_at).toLocaleDateString()} - + + Series Feedback & Clinical Insights + + + {/* Summary Statistics */} + + + + Total Series + {patientData.series_summary.length} - - - - Last updated on {new Date(patientData.last_processed_at).toLocaleDateString()} - + + + Total Predictions + {patientData.total_predictions} - - - - Status: {patientData.patient_info.status} case - - - - - - Total AI predictions: {patientData.total_predictions} - + + + Processing Status + {patientData.patient_info.status} - - - Notes - - Patient case processed with {patientData.series_summary.length} DICOM series. - AI analysis completed with {patientData.total_predictions} predictions. - - + + {/* Series Feedback Cards */} + {patientData.series_summary.map((series, seriesIndex) => { + // Get predictions for this series + const seriesPredictions = patientData.predictions_by_series[series.series_num] || []; + const hasPredictions = seriesPredictions.length > 0; + const feedbackslength = patientData.feedback_by_series[series.series_num] || []; + console.log(patientData); + return ( + handleNavigateToFeedbackDetail(series)} + activeOpacity={0.7} + > + + + + Series {series.series_num}: {series.series_description} + + {hasPredictions && ( + + + + {feedbackslength.length} + + + )} + + + {series.total_images} images • {series.modality} modality + {hasPredictions && ` • ${seriesPredictions.length} AI predictions`} + + + + + + Series Number: + {series.series_num} + + + Description: + + {series.series_description} + + + + Total Images: + {series.total_images} + + + Modality: + {series.modality} + + + AI Predictions: + + {hasPredictions ? `${seriesPredictions.length} found` : 'None'} + + + + + + + View Feedback Details + + + ); + })} ); }; @@ -988,11 +1303,374 @@ const PatientDetailsScreen: React.FC = ({ navigation, {/* AI Analysis Tab: Merged view of DICOM images and AI predictions */} {activeTab === 'aiAnalysis' && renderAIAnalysisTab()} - {/* History Tab: Processing history and notes */} - {activeTab === 'history' && renderHistoryTab()} + {/* Feedbacks Tab: Series feedback and clinical insights */} + {activeTab === 'feedbacks' && renderFeedbacksTab()} + {/* DICOM Viewer Modal */} + {selectedDicomData && ( + + )} + {/* Additional Analysis Modal */} + {selectedSeriesForAnalysis && ( + + + + + Additional Analysis - Series {selectedSeriesForAnalysis.series.series_num} + + + + + + + + {/* Series Information */} + + + {selectedSeriesForAnalysis.series.series_description} + + + {selectedSeriesForAnalysis.series.total_images} images • {selectedSeriesForAnalysis.series.modality} modality + + + + {/* AI Predictions Analysis */} + {selectedSeriesForAnalysis.predictions.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_type} + + + + Severity: + + {prediction.prediction.primary_severity} + + + + Location: + + {prediction.prediction.anatomical_location} + + + + + {/* Additional Findings from detailed_results */} + {prediction.prediction.detailed_results && ( + + Additional Analysis Results + + {/* Stroke Detection */} + {prediction.prediction.detailed_results.stroke_detection && ( + + Stroke Detection: + + + Normal: {((prediction.prediction.detailed_results.stroke_detection.Normal || 0) * 100).toFixed(1)}% + + + Stroke: {((prediction.prediction.detailed_results.stroke_detection.Stroke || 0) * 100).toFixed(1)}% + + + + )} + + {/* Binary Hemorrhage */} + {prediction.prediction.detailed_results.binary_hemorrhage && ( + + Hemorrhage Detection: + + + Normal: {((prediction.prediction.detailed_results.binary_hemorrhage.Normal || 0) * 100).toFixed(1)}% + + + Hemorrhage: {((prediction.prediction.detailed_results.binary_hemorrhage.Hemorrhage || 0) * 100).toFixed(1)}% + + + + )} + + )} + + {/* Visual Indicators Section - Show detailed findings with percentage indicators */} + + Detailed Findings Analysis + + {/* Compact Hemorrhage Type Percentage Indicators with Progress Bars */} + + {/* Epidural */} + + + + Epidural + + {getPercentageValue(prediction.prediction, 'epidural').toFixed(1)}% + + + + + + + + {/* Subdural */} + + + + Subdural + + {getPercentageValue(prediction.prediction, 'subdural').toFixed(1)}% + + + + + + + + {/* Intraparenchymal */} + + + + Intraparenchymal + + {getPercentageValue(prediction.prediction, 'intraparenchymal').toFixed(1)}% + + + + + + + + {/* Subarachnoid */} + + + + Subarachnoid + + {getPercentageValue(prediction.prediction, 'subarachnoid').toFixed(1)}% + + + + + + + + {/* Intraventricular */} + + + + Intraventricular + + {getPercentageValue(prediction.prediction, 'intraventricular').toFixed(1)}% + + + + + + + + {/* Midline Shift */} + + + + Midline Shift + + {getPercentageValue(prediction.prediction, 'midline_shift').toFixed(1)}% + + + + 50 ? theme.colors.error : theme.colors.warning + } + ]} + /> + + + + + {/* Summary Indicator with Overall Percentage */} + + Overall Assessment + + {/* Overall Percentage Calculation */} + {(() => { + const percentages = [ + getPercentageValue(prediction.prediction, 'epidural'), + getPercentageValue(prediction.prediction, 'subdural'), + getPercentageValue(prediction.prediction, 'intraparenchymal'), + getPercentageValue(prediction.prediction, 'subarachnoid'), + getPercentageValue(prediction.prediction, 'intraventricular'), + getPercentageValue(prediction.prediction, 'midline_shift') + ]; + const maxPercentage = Math.max(...percentages); + const hasHemorrhage = maxPercentage > 10; // Consider >10% as detected + + return ( + + Highest Detection: + {maxPercentage.toFixed(1)}% + + + + + {hasHemorrhage ? 'HEMORRHAGE DETECTED' : 'NO HEMORRHAGE'} + + + + ); + })()} + + + + + Processed: {new Date(prediction.processed_at).toLocaleDateString()} + + + ))} + + + + + Close + + + + + )} ); @@ -1242,22 +1920,7 @@ const styles = StyleSheet.create({ color: theme.colors.textPrimary, fontFamily: theme.typography.fontFamily.bold, }, - seriesDetailButton: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: theme.colors.backgroundAlt, - paddingHorizontal: 10, - paddingVertical: 6, - borderRadius: 16, - borderWidth: 1, - borderColor: theme.colors.border, - }, - seriesDetailButtonText: { - fontSize: 12, - color: theme.colors.primary, - fontFamily: theme.typography.fontFamily.medium, - marginLeft: 4, - }, + seriesMeta: { fontSize: 14, color: theme.colors.textSecondary, @@ -1365,15 +2028,14 @@ const styles = StyleSheet.create({ // Series Details Styles seriesDetails: { backgroundColor: theme.colors.backgroundAlt, - padding: theme.spacing.sm, + padding: theme.spacing.md, borderRadius: 8, - marginBottom: theme.spacing.md, }, seriesDetailItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', - marginBottom: theme.spacing.xs, + marginBottom: theme.spacing.sm, }, seriesDetailLabel: { fontSize: 12, @@ -1581,7 +2243,382 @@ const styles = StyleSheet.create({ marginTop: theme.spacing.xs, }, + // New styles for Additional Analysis Button + additionalAnalysisButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.backgroundAlt, + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 16, + borderWidth: 1, + borderColor: theme.colors.border, + }, + additionalAnalysisButtonText: { + fontSize: 12, + color: theme.colors.primary, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: 4, + }, + + // 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, + }, + analysisModal: { + backgroundColor: theme.colors.background, + borderRadius: 12, + width: '90%', + height: '80%', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 4, + elevation: 5, + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.border, + }, + modalTitle: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + closeButton: { + padding: theme.spacing.sm, + }, + modalContent: { + flex: 1, + padding: theme.spacing.md, + }, + modalSeriesInfo: { + marginBottom: theme.spacing.md, + }, + modalSeriesTitle: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.xs, + }, + modalSeriesMeta: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + modalPredictionCard: { + 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, + marginBottom: 28, + }, + modalPredictionHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + modalPredictionLabel: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + modalUrgencyBadge: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 12, + }, + modalUrgencyText: { + fontSize: 10, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + textTransform: 'uppercase', + }, + modalPredictionDetails: { + marginBottom: theme.spacing.sm, + }, + modalPredictionDetailItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.xs, + }, + modalPredictionDetailLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + flex: 1, + }, + modalPredictionDetailValue: { + fontSize: 12, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + flex: 2, + textAlign: 'right', + }, + modalPredictionTimestamp: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + textAlign: 'right', + }, + modalAdditionalFindingsSection: { + marginTop: theme.spacing.sm, + paddingTop: theme.spacing.sm, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + modalAdditionalFindingsTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.xs, + }, + modalAdditionalFindingItem: { + marginBottom: theme.spacing.sm, + }, + modalAdditionalFindingLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginBottom: theme.spacing.xs, + }, + modalAdditionalFindingValues: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + modalAdditionalFindingValue: { + fontSize: 12, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + }, + modalVisualIndicatorsSection: { + marginTop: theme.spacing.sm, + paddingTop: theme.spacing.sm, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + modalVisualIndicatorsTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.sm, + }, + modalCompactIndicatorsContainer: { + flexDirection: 'column', + marginBottom: theme.spacing.sm, + }, + modalCompactIndicatorItem: { + width: '100%', + paddingVertical: theme.spacing.sm, + paddingHorizontal: theme.spacing.sm, + backgroundColor: theme.colors.background, + borderRadius: 8, + marginBottom: theme.spacing.sm, + borderWidth: 1, + borderColor: theme.colors.border, + }, + modalIndicatorHeader: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.xs, + }, + modalCompactIndicatorTitle: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + marginLeft: theme.spacing.sm, + }, + modalCompactIndicatorPercentage: { + fontSize: 12, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: theme.spacing.sm, + }, + modalProgressBarContainer: { + height: 8, + backgroundColor: theme.colors.border, + borderRadius: 4, + overflow: 'hidden', + }, + modalProgressBar: { + height: '100%', + borderRadius: 4, + }, + modalSummaryIndicator: { + marginTop: theme.spacing.sm, + paddingTop: theme.spacing.sm, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + modalSummaryIndicatorTitle: { + fontSize: 14, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + marginBottom: theme.spacing.xs, + }, + modalOverallPercentageContainer: { + // flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: theme.spacing.sm, + }, + modalOverallPercentageLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + }, + modalOverallPercentageValue: { + fontSize: 18, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + }, + modalSummaryIndicatorStatus: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: theme.spacing.xs, + paddingHorizontal: theme.spacing.sm, + borderRadius: 12, + marginTop: theme.spacing.xs, + }, + modalSummaryIndicatorText: { + fontSize: 12, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: theme.spacing.sm, + }, + modalFooter: { + padding: theme.spacing.md, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + modalCloseButton: { + backgroundColor: theme.colors.primary, + paddingVertical: theme.spacing.md, + paddingHorizontal: theme.spacing.lg, + borderRadius: 8, + alignItems: 'center', + }, + modalCloseButtonText: { + color: theme.colors.background, + fontSize: 16, + fontFamily: theme.typography.fontFamily.bold, + }, + + // New styles for Feedback Series Cards + feedbackSeriesCard: { + backgroundColor: theme.colors.background, + borderRadius: 16, + padding: 20, + marginBottom: theme.spacing.md, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 4, + elevation: 2, + borderWidth: 1, + borderColor: theme.colors.border, + }, + feedbackSeriesHeader: { + marginBottom: theme.spacing.md, + }, + feedbackSeriesTitleRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginBottom: 4, + }, + feedbackSeriesTitle: { + fontSize: 16, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.bold, + flex: 1, + marginRight: theme.spacing.sm, + }, + feedbackCountBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: theme.colors.primary, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 12, + minWidth: 24, + justifyContent: 'center', + }, + feedbackCountText: { + fontSize: 14, + color: theme.colors.background, + fontFamily: theme.typography.fontFamily.bold, + marginLeft: 4, + }, + feedbackSeriesMeta: { + fontSize: 14, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.regular, + }, + feedbackSeriesDetails: { + backgroundColor: theme.colors.backgroundAlt, + padding: theme.spacing.md, + borderRadius: 8, + marginBottom: theme.spacing.md, + }, + feedbackSeriesDetailItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + marginBottom: theme.spacing.sm, + }, + feedbackSeriesDetailLabel: { + fontSize: 12, + color: theme.colors.textSecondary, + fontFamily: theme.typography.fontFamily.medium, + flex: 1, + }, + feedbackSeriesDetailValue: { + fontSize: 12, + color: theme.colors.textPrimary, + fontFamily: theme.typography.fontFamily.regular, + flex: 2, + textAlign: 'right', + }, + feedbackSeriesFooter: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingTop: theme.spacing.sm, + borderTopWidth: 1, + borderTopColor: theme.colors.border, + }, + feedbackSeriesFooterText: { + fontSize: 14, + color: theme.colors.primary, + fontFamily: theme.typography.fontFamily.medium, + marginLeft: theme.spacing.xs, + }, }); 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 e58a606..55e3ade 100644 --- a/app/modules/PatientCare/screens/PatientsScreen.tsx +++ b/app/modules/PatientCare/screens/PatientsScreen.tsx @@ -65,7 +65,7 @@ import { selectUser } from '../../Auth/redux/authSelectors'; * Features: * - Real-time patient data fetching * - Search functionality with real-time filtering - * - Filter tabs (All, Processed, Pending, Error) + * - Filter tabs (All, Processed, Pending) * - Sort options (Date, Name, Processed) * - Pull-to-refresh functionality * - Patient cards with vital information @@ -152,7 +152,7 @@ const PatientsScreen: React.FC = () => { * * Purpose: Update the selected filter and refresh the list */ - const handleFilterChange = useCallback((filter: 'all' | 'processed' | 'pending' | 'error') => { + const handleFilterChange = useCallback((filter: 'all' | 'processed' | 'pending') => { dispatch(setFilter(filter)); }, [dispatch]); diff --git a/app/modules/PatientCare/screens/SeriesDetailScreen.tsx b/app/modules/PatientCare/screens/SeriesDetailScreen.tsx index 40cfb39..5f3af3b 100644 --- a/app/modules/PatientCare/screens/SeriesDetailScreen.tsx +++ b/app/modules/PatientCare/screens/SeriesDetailScreen.tsx @@ -33,6 +33,7 @@ import { theme } from '../../../theme/theme'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import Icon from 'react-native-vector-icons/Feather'; import { SafeAreaView } from 'react-native-safe-area-context'; +import { DicomViewerModal } from '../../../shared/components'; // Import types and API import { patientAPI } from '../services/patientAPI'; @@ -151,6 +152,14 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou // Track newly added feedback for visual indication const [newFeedbackIds, setNewFeedbackIds] = useState>(new Set()); + + // DICOM Modal state + const [dicomModalVisible, setDicomModalVisible] = useState(false); + const [selectedDicomData, setSelectedDicomData] = useState<{ + dicomUrl: string; + prediction: Prediction; + imageIndex: number; + } | null>(null); // ============================================================================ // EFFECTS @@ -218,6 +227,44 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou navigation.goBack(); }, [navigation]); + /** + * Handle DICOM Image Press + * + * Purpose: Open DICOM viewer modal for selected prediction image + * + * @param prediction - Prediction data containing DICOM file path + * @param imageIndex - Index of the image in the series + */ + const handleDicomImagePress = useCallback((prediction: Prediction, imageIndex: number) => { + if (prediction?.file_path) { + const dicomUrl = API_CONFIG.BASE_URL + '/api/dicom' + prediction.file_path; + console.log('DICOM URL:', dicomUrl); + + setSelectedDicomData({ + dicomUrl, + prediction, + imageIndex, + }); + setDicomModalVisible(true); + } else { + Alert.alert( + 'No DICOM Available', + 'No DICOM file path is available for this image.', + [{ text: 'OK' }] + ); + } + }, []); + + /** + * Handle Close DICOM Modal + * + * Purpose: Close DICOM viewer modal and reset state + */ + const handleCloseDicomModal = useCallback(() => { + setDicomModalVisible(false); + setSelectedDicomData(null); + }, []); + /** * Handle Refresh * @@ -564,25 +611,6 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou return 0; }; - /** - * Get Adjusted Percentage Value - * - * Purpose: Show 100% when there's only 1 image in the series to indicate full analysis - * - * @param prediction - Prediction object - * @param type - Hemorrhage type (epidural, subdural, etc.) - */ - const getAdjustedPercentageValue = (prediction: any, type: string): number => { - const basePercentage = getPercentageValue(prediction, type); - - // If there's only 1 image in the series, show 100% to indicate full analysis - if (seriesData && seriesData.total_images === 1) { - return 100; - } - - return basePercentage; - }; - /** * Get Series Predictions * @@ -1067,14 +1095,14 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou Epidural - {getAdjustedPercentageValue(prediction.prediction, 'epidural').toFixed(1)}% + {getPercentageValue(prediction.prediction, 'epidural').toFixed(1)}% @@ -1082,8 +1110,8 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou style={[ styles.progressBar, { - width: `${getAdjustedPercentageValue(prediction.prediction, 'epidural')}%`, - backgroundColor: getPercentageColor(getAdjustedPercentageValue(prediction.prediction, 'epidural')) + width: `${getPercentageValue(prediction.prediction, 'epidural')}%`, + backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'epidural')) } ]} /> @@ -1096,14 +1124,14 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou Subdural - {getAdjustedPercentageValue(prediction.prediction, 'subdural').toFixed(1)}% + {getPercentageValue(prediction.prediction, 'subdural').toFixed(1)}% @@ -1111,8 +1139,8 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou style={[ styles.progressBar, { - width: `${getAdjustedPercentageValue(prediction.prediction, 'subdural')}%`, - backgroundColor: getPercentageColor(getAdjustedPercentageValue(prediction.prediction, 'subdural')) + width: `${getPercentageValue(prediction.prediction, 'subdural')}%`, + backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'subdural')) } ]} /> @@ -1125,14 +1153,14 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou Intraparenchymal - {getAdjustedPercentageValue(prediction.prediction, 'intraparenchymal').toFixed(1)}% + {getPercentageValue(prediction.prediction, 'intraparenchymal').toFixed(1)}% @@ -1140,8 +1168,8 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou style={[ styles.progressBar, { - width: `${getAdjustedPercentageValue(prediction.prediction, 'intraparenchymal')}%`, - backgroundColor: getPercentageColor(getAdjustedPercentageValue(prediction.prediction, 'intraparenchymal')) + width: `${getPercentageValue(prediction.prediction, 'intraparenchymal')}%`, + backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'intraparenchymal')) } ]} /> @@ -1154,14 +1182,14 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou Subarachnoid - {getAdjustedPercentageValue(prediction.prediction, 'subarachnoid').toFixed(1)}% + {getPercentageValue(prediction.prediction, 'subarachnoid').toFixed(1)}% @@ -1169,8 +1197,8 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou style={[ styles.progressBar, { - width: `${getAdjustedPercentageValue(prediction.prediction, 'subarachnoid')}%`, - backgroundColor: getPercentageColor(getAdjustedPercentageValue(prediction.prediction, 'subarachnoid')) + width: `${getPercentageValue(prediction.prediction, 'subarachnoid')}%`, + backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'subarachnoid')) } ]} /> @@ -1183,14 +1211,14 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou Intraventricular - {getAdjustedPercentageValue(prediction.prediction, 'intraventricular').toFixed(1)}% + {getPercentageValue(prediction.prediction, 'intraventricular').toFixed(1)}% @@ -1198,8 +1226,8 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou style={[ styles.progressBar, { - width: `${getAdjustedPercentageValue(prediction.prediction, 'intraventricular')}%`, - backgroundColor: getPercentageColor(getAdjustedPercentageValue(prediction.prediction, 'intraventricular')) + width: `${getPercentageValue(prediction.prediction, 'intraventricular')}%`, + backgroundColor: getPercentageColor(getPercentageValue(prediction.prediction, 'intraventricular')) } ]} /> @@ -1212,14 +1240,14 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou Midline Shift - {getAdjustedPercentageValue(prediction.prediction, 'midline_shift').toFixed(1)}% + {getPercentageValue(prediction.prediction, 'midline_shift').toFixed(1)}% @@ -1227,8 +1255,8 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou style={[ styles.progressBar, { - width: `${getAdjustedPercentageValue(prediction.prediction, 'midline_shift')}%`, - backgroundColor: getAdjustedPercentageValue(prediction.prediction, 'midline_shift') > 50 ? theme.colors.error : theme.colors.warning + width: `${getPercentageValue(prediction.prediction, 'midline_shift')}%`, + backgroundColor: getPercentageValue(prediction.prediction, 'midline_shift') > 50 ? theme.colors.error : theme.colors.warning } ]} /> @@ -1243,12 +1271,12 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou {/* Overall Percentage Calculation */} {(() => { const percentages = [ - getAdjustedPercentageValue(prediction.prediction, 'epidural'), - getAdjustedPercentageValue(prediction.prediction, 'subdural'), - getAdjustedPercentageValue(prediction.prediction, 'intraparenchymal'), - getAdjustedPercentageValue(prediction.prediction, 'subarachnoid'), - getAdjustedPercentageValue(prediction.prediction, 'intraventricular'), - getAdjustedPercentageValue(prediction.prediction, 'midline_shift') + getPercentageValue(prediction.prediction, 'epidural'), + getPercentageValue(prediction.prediction, 'subdural'), + getPercentageValue(prediction.prediction, 'intraparenchymal'), + getPercentageValue(prediction.prediction, 'subarachnoid'), + getPercentageValue(prediction.prediction, 'intraventricular'), + getPercentageValue(prediction.prediction, 'midline_shift') ]; const maxPercentage = Math.max(...percentages); const hasHemorrhage = maxPercentage > 10; // Consider >10% as detected @@ -1356,11 +1384,22 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou {predictions.map((prediction: Prediction, index: number) => ( {prediction.preview ? ( - + handleDicomImagePress(prediction, index)} + activeOpacity={0.7} + > + + {/* Overlay to indicate clickable */} + + + View DICOM + + ) : ( @@ -1723,6 +1762,19 @@ const SeriesDetailScreen: React.FC = ({ navigation, rou )} + + {/* DICOM Viewer Modal */} + {selectedDicomData && ( + + )} + ); }; @@ -1953,11 +2005,34 @@ const styles = StyleSheet.create({ alignItems: 'center', marginRight: theme.spacing.md, }, + imageClickable: { + position: 'relative', + marginBottom: theme.spacing.xs, + borderRadius: 8, + overflow: 'hidden', + }, seriesImage: { width: 120, height: 120, borderRadius: 8, - marginBottom: theme.spacing.xs, + }, + imageOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + justifyContent: 'center', + alignItems: 'center', + opacity: 1, + }, + imageOverlayText: { + color: theme.colors.background, + fontSize: 10, + fontFamily: theme.typography.fontFamily.bold, + marginTop: 4, + textAlign: 'center', }, noImagePlaceholder: { width: 120, diff --git a/app/modules/PatientDetailsScreen.tsx b/app/modules/PatientDetailsScreen.tsx new file mode 100644 index 0000000..b28b04f --- /dev/null +++ b/app/modules/PatientDetailsScreen.tsx @@ -0,0 +1,3 @@ + + + diff --git a/app/modules/Settings/screens/AppInfoScreen.tsx b/app/modules/Settings/screens/AppInfoScreen.tsx index 319abf1..3f71dbb 100644 --- a/app/modules/Settings/screens/AppInfoScreen.tsx +++ b/app/modules/Settings/screens/AppInfoScreen.tsx @@ -54,7 +54,7 @@ export const AppInfoScreen: React.FC = ({ // App version and build information const appInfo = { - name: 'NeoScan Radiologist', + name: 'NeoScan Physician', version: '1.0.0', buildNumber: '2025.08.001', releaseDate: 'August 2025', diff --git a/app/modules/Settings/screens/SettingsScreen.tsx b/app/modules/Settings/screens/SettingsScreen.tsx index 27ee3e5..1a88e4d 100644 --- a/app/modules/Settings/screens/SettingsScreen.tsx +++ b/app/modules/Settings/screens/SettingsScreen.tsx @@ -18,6 +18,7 @@ import { ActivityIndicator, ActionSheetIOS, Platform, + PermissionsAndroid, } from 'react-native'; import { theme } from '../../../theme/theme'; import { @@ -31,6 +32,7 @@ import { ProfileCard } from '../components/ProfileCard'; import { CustomModal } from '../../../shared/components'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { logoutUser } from '../../Auth/redux/authActions'; +import { updateUserProfile } from '../../Auth/redux/authSlice'; import { selectUser, selectUserDisplayName, @@ -99,6 +101,15 @@ export const SettingsScreen: React.FC = ({ // Profile photo state const [uploadingPhoto, setUploadingPhoto] = useState(false); const [tempProfilePhoto, setTempProfilePhoto] = useState(null); + + // Upload response interface + interface UploadPhotoResponse { + success: boolean; + message?: string; + data?: { + profile_photo_url: string; + }; + } // Modal state const [modalVisible, setModalVisible] = useState(false); @@ -213,6 +224,39 @@ export const SettingsScreen: React.FC = ({ setSettingsSections(generateSettingsSections()); }, [user, dashboardSettings]); + // ============================================================================ + // PERMISSION HANDLERS + // ============================================================================ + + /** + * requestCameraPermission Function + * + * Purpose: Request camera permission for Android devices + * + * @returns Promise - Whether permission was granted + */ + const requestCameraPermission = async (): Promise => { + if (Platform.OS === 'android') { + try { + const granted = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.CAMERA, + { + title: 'Camera Permission', + message: 'This app needs camera permission to capture profile photos.', + buttonNeutral: 'Ask Me Later', + buttonNegative: 'Cancel', + buttonPositive: 'OK', + } + ); + return granted === PermissionsAndroid.RESULTS.GRANTED; + } catch (err) { + console.warn('Camera permission error:', err); + return false; + } + } + return true; // iOS permissions are handled via Info.plist + }; + // ============================================================================ // EVENT HANDLERS // ============================================================================ @@ -263,48 +307,92 @@ export const SettingsScreen: React.FC = ({ * Purpose: Launch camera to capture new profile photo * * Flow: - * 1. Launch camera - * 2. Validate captured image - * 3. Upload to server - * 4. Update local state + * 1. Check camera permissions + * 2. Launch camera with callback + * 3. Validate captured image + * 4. Upload to server + * 5. Update local state */ const handleCameraCapture = async () => { try { - // Launch camera - const result: ImagePickerResponse = await launchCamera({ + // Check camera permission first + const hasPermission = await requestCameraPermission(); + + if (!hasPermission) { + setModalConfig({ + title: 'Permission Required', + message: 'Camera permission is required to capture profile photos.', + type: 'error', + onConfirm: () => {}, + showCancel: false, + icon: 'camera', + }); + setModalVisible(true); + return; + } + + // Launch camera with callback + const options = { mediaType: 'photo' as MediaType, - quality: 0.8, + quality: 0.8 as const, maxWidth: 800, maxHeight: 800, saveToPhotos: false, includeBase64: false, + }; + + launchCamera(options, (response: ImagePickerResponse) => { + try { + // Handle user cancellation + if (response.didCancel) { + return; + } + + // Handle errors + if (response.errorMessage) { + throw new Error(response.errorMessage); + } + + // Validate response and assets + if (!response.assets || response.assets.length === 0) { + throw new Error('No image captured'); + } + + const asset = response.assets[0]; + if (!asset.uri) { + throw new Error('Invalid image data'); + } + + // Validate file size (max 5MB) + if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) { + throw new Error('Image size must be less than 5MB'); + } + + // Set temporary photo for preview + setTempProfilePhoto(asset.uri); + + // Upload the captured photo + uploadProfilePhoto(asset.uri); + + } catch (error) { + console.error('Camera capture processing error:', error); + setModalConfig({ + title: 'Camera Error', + message: error instanceof Error ? error.message : 'Failed to capture photo', + type: 'error', + onConfirm: () => {}, + showCancel: false, + icon: 'alert-circle', + }); + setModalVisible(true); + } }); - if (result.didCancel || !result.assets || result.assets.length === 0) { - return; - } - - const asset = result.assets[0]; - if (!asset.uri) { - throw new Error('No image captured'); - } - - // Validate file size (max 5MB) - if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) { - throw new Error('Image size must be less than 5MB'); - } - - // Set temporary photo for preview - setTempProfilePhoto(asset.uri); - - // Upload the captured photo - await uploadProfilePhoto(asset.uri); - } catch (error) { - console.error('Camera capture error:', error); + console.error('Camera launch error:', error); setModalConfig({ title: 'Error', - message: error instanceof Error ? error.message : 'Failed to capture photo', + message: 'Failed to launch camera. Please try again.', type: 'error', onConfirm: () => {}, showCancel: false, @@ -320,47 +408,74 @@ export const SettingsScreen: React.FC = ({ * Purpose: Launch gallery to select existing profile photo * * Flow: - * 1. Launch image picker + * 1. Launch image picker with callback * 2. Validate selected image * 3. Upload to server * 4. Update local state */ - const handleGallerySelection = async () => { + const handleGallerySelection = () => { try { - // Launch image picker - const result: ImagePickerResponse = await launchImageLibrary({ + // Launch image picker with callback + const options = { mediaType: 'photo' as MediaType, - quality: 0.8, + quality: 0.8 as const, maxWidth: 800, maxHeight: 800, includeBase64: false, + }; + + launchImageLibrary(options, (response: ImagePickerResponse) => { + try { + // Handle user cancellation + if (response.didCancel) { + return; + } + + // Handle errors + if (response.errorMessage) { + throw new Error(response.errorMessage); + } + + // Validate response and assets + if (!response.assets || response.assets.length === 0) { + throw new Error('No image selected'); + } + + const asset = response.assets[0]; + if (!asset.uri) { + throw new Error('Invalid image data'); + } + + // Validate file size (max 5MB) + if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) { + throw new Error('Image size must be less than 5MB'); + } + + // Set temporary photo for preview + setTempProfilePhoto(asset.uri); + + // Upload the selected photo + uploadProfilePhoto(asset.uri); + + } catch (error) { + console.error('Gallery selection processing error:', error); + setModalConfig({ + title: 'Gallery Error', + message: error instanceof Error ? error.message : 'Failed to select photo', + type: 'error', + onConfirm: () => {}, + showCancel: false, + icon: 'alert-circle', + }); + setModalVisible(true); + } }); - if (result.didCancel || !result.assets || result.assets.length === 0) { - return; - } - - const asset = result.assets[0]; - if (!asset.uri) { - throw new Error('No image selected'); - } - - // Validate file size (max 5MB) - if (asset.fileSize && asset.fileSize > 5 * 1024 * 1024) { - throw new Error('Image size must be less than 5MB'); - } - - // Set temporary photo for preview - setTempProfilePhoto(asset.uri); - - // Upload the selected photo - await uploadProfilePhoto(asset.uri); - } catch (error) { - console.error('Gallery selection error:', error); + console.error('Gallery launch error:', error); setModalConfig({ title: 'Error', - message: error instanceof Error ? error.message : 'Failed to select photo', + message: 'Failed to open gallery. Please try again.', type: 'error', onConfirm: () => {}, showCancel: false, @@ -400,21 +515,30 @@ export const SettingsScreen: React.FC = ({ const response = await authAPI.uploadProfilePhoto(formData, token); // Type the response properly - const responseData = response.data as { success: boolean; message?: string; data?: any }; + const responseData = response.data as UploadPhotoResponse; if (responseData.success) { // Update local state with new photo setTempProfilePhoto(null); + // Update Redux state with new profile photo URL + if (responseData.data?.profile_photo_url) { + console.log('Updating user profile with new photo URL:', responseData.data.profile_photo_url); + dispatch(updateUserProfile({ + self_url: responseData.data.profile_photo_url + })); + console.log('Redux state updated successfully'); + } + // Show success message setModalConfig({ title: 'Success', - message: 'Profile photo updated successfully!', + message: responseData.message || 'Profile photo updated successfully!', type: 'success', icon: 'check-circle', onConfirm: () => { - // Refresh user data or update Redux state - handleRefresh(); + // Optional: Refresh if needed, but Redux update should be enough + // handleRefresh(); }, showCancel: false, }); @@ -571,7 +695,7 @@ export const SettingsScreen: React.FC = ({ }; -console.log('user', user) + // ============================================================================ // MAIN RENDER // ============================================================================ @@ -610,9 +734,9 @@ console.log('user', user) style={styles.profileImage} resizeMode="cover" /> - ) : user.profile_photo_url ? ( + ) : user.self_url ? ( @@ -645,19 +769,19 @@ console.log('user', user) {user.display_name || `${user.first_name} ${user.last_name}`} {user.email} - Radiologist + Physician )} {/* Settings sections */} - {settingsSections.map((section) => ( - - ))} + {settingsSections.map((section, index) => + React.createElement(SettingsSectionComponent, { + key: `${section.id}-${index}`, + section: section + }) + )} {/* Bottom spacing for tab bar */} diff --git a/app/navigation/navigationTypes.ts b/app/navigation/navigationTypes.ts index 80c2cc6..0f3476f 100644 --- a/app/navigation/navigationTypes.ts +++ b/app/navigation/navigationTypes.ts @@ -7,6 +7,7 @@ import { NavigatorScreenParams } from '@react-navigation/native'; import { ERDashboard, Patient, Alert as AlertType } from '../shared/types'; +import { PatientCareStackParamList } from '../modules/PatientCare/navigation/navigationTypes'; // ============================================================================ // ROOT NAVIGATION TYPES @@ -44,7 +45,7 @@ export type RootStackParamList = { */ export type MainTabParamList = { Dashboard: DashboardScreenParams; // Dashboard with initial data - Patients: PatientsScreenParams; // Patient list screen + Patients: NavigatorScreenParams; // Patient care stack navigator AIPredictions: AIPredictionScreenParams; // AI predictions screen Settings: SettingsScreenParams; // Settings screen }; diff --git a/app/shared/components/DicomViewer.tsx b/app/shared/components/DicomViewer.tsx index 9d0580a..b6ffb9a 100644 --- a/app/shared/components/DicomViewer.tsx +++ b/app/shared/components/DicomViewer.tsx @@ -14,7 +14,6 @@ interface DicomViewerProps { dicomUrl: string; onError?: (error: string) => void; onLoad?: () => void; - debugMode?: boolean; } // Interface for WebView reference @@ -23,40 +22,24 @@ interface WebViewRef { reload: () => void; } -export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = false }: DicomViewerProps): React.ReactElement { +export default function DicomViewer({ dicomUrl, onError, onLoad }: DicomViewerProps): React.ReactElement { const webViewRef = useRef(null); - const [isLoading, setIsLoading] = useState(true); const [hasError, setHasError] = useState(false); - const [debugInfo, setDebugInfo] = useState([]); const [webViewReady, setWebViewReady] = useState(false); - // Debug logging function - const debugLog = (message: string) => { - if (debugMode) { - const timestamp = new Date().toLocaleTimeString(); - const logMessage = `[${timestamp}] ${message}`; - console.log(logMessage); - setDebugInfo(prev => [...prev.slice(-9), logMessage]); // Keep last 10 messages - } - }; + // Handle WebView load events const handleLoadStart = () => { - debugLog('WebView load started'); - setIsLoading(true); setHasError(false); }; const handleLoadEnd = () => { - debugLog('WebView load ended'); - setIsLoading(false); setWebViewReady(true); onLoad?.(); }; const handleError = (error: any) => { - debugLog(`WebView error: ${JSON.stringify(error)}`); - setIsLoading(false); setHasError(true); onError?.(error?.nativeEvent?.description || 'Failed to load DICOM viewer'); }; @@ -64,13 +47,11 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal const handleMessage = (event: WebViewMessageEvent) => { try { const message = event.nativeEvent.data; - debugLog(`Message from WebView: ${message}`); // Try to parse JSON message if (typeof message === 'string') { try { const parsedMessage = JSON.parse(message); - debugLog(`Parsed message: ${JSON.stringify(parsedMessage)}`); if (parsedMessage.type === 'error') { setHasError(true); @@ -79,26 +60,23 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal setHasError(false); } } catch (parseError) { - debugLog(`Failed to parse message as JSON: ${parseError}`); + // Failed to parse message as JSON } } } catch (error) { - debugLog(`Error handling WebView message: ${error}`); + // Error handling WebView message } }; // Send DICOM URL to WebView when component mounts or URL changes useEffect(() => { if (webViewRef.current && dicomUrl && webViewReady) { - debugLog(`Sending DICOM URL to WebView: ${dicomUrl}`); - // Wait a bit for WebView to be ready const timer = setTimeout(() => { if (webViewRef.current) { try { // Send the URL directly as a string message webViewRef.current.postMessage(dicomUrl); - debugLog('DICOM URL sent successfully'); // Also try sending as a structured message setTimeout(() => { @@ -108,12 +86,11 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal data: dicomUrl }); webViewRef.current.postMessage(structuredMessage); - debugLog('Structured DICOM message sent'); } }, 500); } catch (error) { - debugLog(`Failed to send DICOM URL: ${error}`); + // Failed to send DICOM URL } } }, 1000); @@ -124,25 +101,20 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal // Reload WebView if there's an error const handleRetry = () => { - debugLog('Retrying WebView load'); if (webViewRef.current) { setHasError(false); - setIsLoading(true); setWebViewReady(false); webViewRef.current.reload(); } }; - // Clear debug info - const clearDebugInfo = () => { - setDebugInfo([]); - }; + return ( ( - - - Loading DICOM Viewer... - - )} + mixedContentMode="always" /> {hasError && ( @@ -175,32 +141,7 @@ export default function DicomViewer({ dicomUrl, onError, onLoad, debugMode = fal )} - {debugMode && ( - - - Debug Info - - Clear - - - - {debugInfo.map((info, index) => ( - {info} - ))} - - - - WebView Ready: {webViewReady ? 'Yes' : 'No'} - - - Loading: {isLoading ? 'Yes' : 'No'} - - - Error: {hasError ? 'Yes' : 'No'} - - - - )} + ); } @@ -214,21 +155,7 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: '#000', }, - loadingContainer: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - bottom: 0, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#000', - }, - loadingText: { - color: '#FFF', - marginTop: 16, - fontSize: 16, - }, + errorContainer: { position: 'absolute', top: 0, @@ -265,52 +192,6 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '600', }, - debugContainer: { - position: 'absolute', - top: 10, - right: 10, - backgroundColor: 'rgba(0,0,0,0.9)', - borderRadius: 8, - padding: 10, - maxWidth: 300, - maxHeight: 400, - }, - debugHeader: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - marginBottom: 8, - }, - debugTitle: { - color: '#FFFFFF', - fontSize: 14, - fontWeight: '600', - }, - clearButton: { - color: '#2196F3', - fontSize: 12, - textDecorationLine: 'underline', - }, - debugContent: { - maxHeight: 200, - }, - debugText: { - color: '#FFFFFF', - fontSize: 10, - fontFamily: 'monospace', - marginBottom: 2, - }, - debugStatus: { - marginTop: 8, - paddingTop: 8, - borderTopColor: '#333', - borderTopWidth: 1, - }, - debugStatusText: { - color: '#CCC', - fontSize: 10, - marginBottom: 2, - }, }); /* diff --git a/app/shared/components/DicomViewerModal.example.tsx b/app/shared/components/DicomViewerModal.example.tsx new file mode 100644 index 0000000..81012be --- /dev/null +++ b/app/shared/components/DicomViewerModal.example.tsx @@ -0,0 +1,241 @@ +/* + * File: DicomViewerModal.example.tsx + * Description: Example usage of DicomViewerModal component + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet } from 'react-native'; +import { DicomViewerModal } from './index'; +import { theme } from '../../theme/theme'; + +// ============================================================================ +// EXAMPLE COMPONENT +// ============================================================================ + +/** + * DicomViewerModalExample Component + * + * Purpose: Demonstrates how to use the DicomViewerModal component + * + * Features: + * - Shows how to pass dicomUrl to modal + * - Demonstrates modal state management + * - Example with patient information + * - Error handling examples + */ +export const DicomViewerModalExample: React.FC = () => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const [isModalVisible, setIsModalVisible] = useState(false); + + // Example DICOM URLs (replace with your actual URLs) + const exampleDicomUrl = 'https://example-dicom-server.com/studies/123/series/456/instances/789'; + + // Example patient data + const patientData = { + name: 'John Doe', + studyDescription: 'CT Brain with Contrast', + }; + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Open DICOM viewer modal + */ + const openDicomViewer = () => { + setIsModalVisible(true); + }; + + /** + * Close DICOM viewer modal + */ + const closeDicomViewer = () => { + setIsModalVisible(false); + }; + + // ============================================================================ + // RENDER + // ============================================================================ + + return ( + + {/* Trigger Button */} + + View DICOM Image + + + {/* DICOM Viewer Modal */} + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: theme.colors.background, + padding: theme.spacing.lg, + }, + + button: { + backgroundColor: theme.colors.primary, + paddingHorizontal: theme.spacing.xl, + paddingVertical: theme.spacing.md, + borderRadius: theme.borderRadius.medium, + ...theme.shadows.primary, + }, + + buttonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, +}); + +// ============================================================================ +// USAGE EXAMPLES IN OTHER COMPONENTS +// ============================================================================ + +/* + +// Example 1: Basic Usage in Patient Details Screen +import { DicomViewerModal } from '../../../shared/components'; + +const PatientDetailsExample = () => { + const [showDicom, setShowDicom] = useState(false); + const dicomUrl = patient.scanResults?.dicomUrl; + + return ( + <> + setShowDicom(true)}> + View Scan Results + + + setShowDicom(false)} + patientName={patient.name} + studyDescription={patient.scanResults?.description} + /> + + ); +}; + +// Example 2: Usage with Series Selection +import { DicomViewerModal } from '../../../shared/components'; + +const SeriesListExample = () => { + const [selectedDicom, setSelectedDicom] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + + const openDicom = (dicomUrl: string) => { + setSelectedDicom(dicomUrl); + setModalVisible(true); + }; + + const closeDicom = () => { + setModalVisible(false); + setSelectedDicom(null); + }; + + return ( + <> + {seriesList.map((series) => ( + openDicom(series.dicomUrl)} + > + {series.description} + + ))} + + + + ); +}; + +// Example 3: Usage with Error Handling +import { DicomViewerModal } from '../../../shared/components'; + +const ErrorHandlingExample = () => { + const [dicomModalState, setDicomModalState] = useState({ + visible: false, + url: '', + title: '', + }); + + const openDicomWithValidation = (url: string, title: string) => { + if (!url) { + Alert.alert('Error', 'No DICOM URL available'); + return; + } + + setDicomModalState({ + visible: true, + url, + title, + }); + }; + + const closeDicomModal = () => { + setDicomModalState({ + visible: false, + url: '', + title: '', + }); + }; + + return ( + <> + openDicomWithValidation(scan.url, scan.title)} + > + View Scan + + + + + ); +}; + +*/ + +/* + * End of File: DicomViewerModal.example.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/shared/components/DicomViewerModal.tsx b/app/shared/components/DicomViewerModal.tsx new file mode 100644 index 0000000..38a6984 --- /dev/null +++ b/app/shared/components/DicomViewerModal.tsx @@ -0,0 +1,344 @@ +/* + * File: DicomViewerModal.tsx + * Description: Reusable modal component for DICOM image viewing + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ + +import React, { useState } from 'react'; +import { + View, + Text, + StyleSheet, + TouchableOpacity, + Modal, + SafeAreaView, + StatusBar, + Dimensions, + Alert, +} from 'react-native'; +import { theme } from '../../theme/theme'; +import Icon from 'react-native-vector-icons/Feather'; +import DicomViewer from './DicomViewer'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * DicomViewerModalProps Interface + * + * Purpose: Defines the props required by the DicomViewerModal component + * + * Props: + * - visible: Whether the modal is visible + * - dicomUrl: URL of the DICOM file to display + * - onClose: Callback function when modal is closed + * - title: Optional title for the modal header + * - patientName: Optional patient name for context + * - studyDescription: Optional study description + */ +interface DicomViewerModalProps { + visible: boolean; + dicomUrl: string; + onClose: () => void; + title?: string; + patientName?: string; + studyDescription?: string; +} + +// ============================================================================ +// DICOM VIEWER MODAL COMPONENT +// ============================================================================ + +/** + * DicomViewerModal Component + * + * Purpose: Provides a full-screen modal for viewing DICOM medical images + * + * Features: + * - Full-screen DICOM image viewing + * - Modal overlay with close functionality + * - Error handling and display + * - Loading states + * - Header with patient/study information + * - Responsive design for different screen sizes + * - Proper medical image viewing environment (dark background) + */ +export const DicomViewerModal: React.FC = ({ + visible, + dicomUrl, + onClose, + title = 'DICOM Viewer', + patientName, + studyDescription, +}) => { + // ============================================================================ + // STATE MANAGEMENT + // ============================================================================ + + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + + // ============================================================================ + // EVENT HANDLERS + // ============================================================================ + + /** + * Handle DICOM viewer load completion + */ + const handleDicomLoad = () => { + setIsLoading(false); + setHasError(false); + }; + + /** + * Handle DICOM viewer errors + * @param error - Error message from DICOM viewer + */ + const handleDicomError = (error: string) => { + setIsLoading(false); + setHasError(true); + + // Show error alert to user + Alert.alert( + 'DICOM Loading Error', + `Failed to load DICOM file: ${error}`, + [ + { + text: 'Retry', + onPress: () => { + setHasError(false); + setIsLoading(true); + }, + }, + { + text: 'Close', + onPress: onClose, + style: 'cancel', + }, + ] + ); + }; + + /** + * Handle modal close request + */ + const handleClose = () => { + // Reset states when closing + setIsLoading(false); + setHasError(false); + onClose(); + }; + + /** + * Handle back button press (Android) + */ + const handleRequestClose = () => { + handleClose(); + }; + + // ============================================================================ + // RENDER HEADER + // ============================================================================ + + /** + * Render modal header with title and close button + */ + const renderHeader = () => ( + + {/* Title and Patient Info */} + + {title} + {patientName && ( + Patient: {patientName} + )} + {studyDescription && ( + Study: {studyDescription} + )} + + + {/* Close Button */} + + + + + ); + + // ============================================================================ + // RENDER CONTENT + // ============================================================================ + + /** + * Render DICOM viewer content + */ + const renderContent = () => { + if (!dicomUrl) { + return ( + + + No DICOM URL provided + + Close + + + ); + } + + return ( + + + + ); + }; + + // ============================================================================ + // MAIN RENDER + // ============================================================================ + + return ( + + + {/* Set status bar to light content for dark background */} + + + {/* Header */} + {renderHeader()} + + {/* Content */} + {renderContent()} + + + ); +}; + +// ============================================================================ +// STYLES +// ============================================================================ + +const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); + +const styles = StyleSheet.create({ + // Main container + container: { + flex: 1, + backgroundColor: '#000000', + }, + + // Header section + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: theme.spacing.lg, + paddingVertical: theme.spacing.md, + backgroundColor: 'rgba(0, 0, 0, 0.9)', + borderBottomWidth: 1, + borderBottomColor: 'rgba(255, 255, 255, 0.1)', + }, + + // Header content + headerContent: { + flex: 1, + marginRight: theme.spacing.md, + }, + + // Header title + headerTitle: { + fontSize: theme.typography.fontSize.displaySmall, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + marginBottom: 2, + }, + + // Header subtitle + headerSubtitle: { + fontSize: theme.typography.fontSize.bodyMedium, + fontFamily: theme.typography.fontFamily.regular, + color: 'rgba(255, 255, 255, 0.8)', + lineHeight: theme.typography.fontSize.bodyMedium * 1.2, + }, + + // Close button + closeButton: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: 'rgba(255, 255, 255, 0.1)', + justifyContent: 'center', + alignItems: 'center', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.2)', + }, + + // DICOM viewer container + viewerContainer: { + flex: 1, + backgroundColor: '#000000', + }, + + // Empty state container + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#000000', + paddingHorizontal: theme.spacing.xl, + }, + + // Empty state text + emptyText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.medium, + color: theme.colors.textMuted, + textAlign: 'center', + marginTop: theme.spacing.md, + marginBottom: theme.spacing.xl, + }, + + // Secondary close button + closeButtonSecondary: { + backgroundColor: theme.colors.primary, + paddingHorizontal: theme.spacing.xl, + paddingVertical: theme.spacing.md, + borderRadius: theme.borderRadius.medium, + ...theme.shadows.primary, + }, + + // Close button text + closeButtonText: { + fontSize: theme.typography.fontSize.bodyLarge, + fontFamily: theme.typography.fontFamily.bold, + color: theme.colors.background, + }, +}); + +// ============================================================================ +// EXPORT +// ============================================================================ + +export default DicomViewerModal; + +/* + * End of File: DicomViewerModal.tsx + * Design & Developed by Tech4Biz Solutions + * Copyright (c) Spurrin Innovations. All rights reserved. + */ diff --git a/app/shared/components/index.ts b/app/shared/components/index.ts index c126ee9..5e1638a 100644 --- a/app/shared/components/index.ts +++ b/app/shared/components/index.ts @@ -18,6 +18,9 @@ export { ComingSoonScreen } from './ComingSoonScreen'; // DICOM Viewer Component export { default as DicomViewer } from './DicomViewer'; +// DICOM Viewer Modal Component +export { default as DicomViewerModal } from './DicomViewerModal'; + // DICOM Viewer Test Component export { default as DicomViewerTest } from './DicomViewerTest'; diff --git a/app/shared/types/auth.ts b/app/shared/types/auth.ts index 21db44d..930f66c 100644 --- a/app/shared/types/auth.ts +++ b/app/shared/types/auth.ts @@ -44,6 +44,7 @@ export interface User { onboarding_message: string; access_token: string; platform: 'app'|'web'; + self_url:string | null; } export type UserRole = diff --git a/app/store/index.ts b/app/store/index.ts index 14cf052..10870a5 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -19,6 +19,7 @@ import settingsReducer from '../modules/Settings/redux/settingsSlice'; import uiReducer from '../modules/Dashboard/redux/uiSlice'; import hospitalReducer from '../modules/Auth/redux/hospitalSlice'; import aiPredictionReducer from '../modules/AIPrediction/redux/aiPredictionSlice'; +import predictionsReducer from '../modules/Dashboard/redux/predictionsSlice'; // ============================================================================ // REDUX PERSIST CONFIGURATION @@ -59,6 +60,7 @@ const persistConfig = { 'alerts', // Temporary alerts and notifications 'dashboard', // Real-time dashboard data 'aiDashboard', // AI dashboard statistics (fetched fresh each time) + 'predictions', // AI predictions data (fetched fresh each time) 'hospital', // Hospital data (fetched fresh each time) ], @@ -90,6 +92,7 @@ const persistConfig = { * - aiDashboard: AI analysis dashboard statistics * - patientCare: Patient information and medical records * - aiPrediction: AI prediction cases and analysis + * - predictions: AI predictions with/without feedback * - alerts: Critical alerts and notifications * - settings: User preferences and app settings * - ui: User interface state (loading, modals, etc.) @@ -100,6 +103,7 @@ const rootReducer = combineReducers({ aiDashboard: aiDashboardReducer, patientCare: patientCareReducer, aiPrediction: aiPredictionReducer, + predictions: predictionsReducer, alerts: alertsReducer, settings: settingsReducer, ui: uiReducer,