633 lines
18 KiB
TypeScript
633 lines
18 KiB
TypeScript
/*
|
|
* 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,
|
|
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<FeedbackDetailScreenProps> = ({ 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<string | null>(null);
|
|
|
|
// ============================================================================
|
|
// EFFECTS
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Component Mount Effect
|
|
*
|
|
* Purpose: Initialize screen and set navigation title
|
|
*/
|
|
useEffect(() => {
|
|
navigation.setOptions({
|
|
title: `Feedback - Series ${seriesNumber}`,
|
|
headerShown: true,
|
|
headerLeft: () => (
|
|
<TouchableOpacity
|
|
style={styles.headerBackButton}
|
|
onPress={handleBackPress}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Icon name="arrow-left" size={24} color={theme.colors.primary} />
|
|
</TouchableOpacity>
|
|
),
|
|
});
|
|
}, [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 (
|
|
<View style={styles.seriesHeader}>
|
|
<View style={styles.seriesHeaderTop}>
|
|
<View style={styles.seriesHeaderLeft}>
|
|
<Text style={styles.seriesTitle}>
|
|
Series {seriesData.series_description}
|
|
</Text>
|
|
<Text style={styles.seriesMeta}>
|
|
{seriesData.total_images} images • {seriesData.modality} modality
|
|
</Text>
|
|
</View>
|
|
<View style={styles.seriesHeaderRight}>
|
|
<View style={styles.seriesStatusBadge}>
|
|
<Icon name="check-circle" size={16} color={'white'} />
|
|
<Text style={styles.seriesStatusText}>Processed</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{patientData && (
|
|
<View style={styles.patientInfoRow}>
|
|
<Icon name="user" size={16} color={theme.colors.textSecondary} />
|
|
<Text style={styles.patientInfoText}>
|
|
{patientData.patient_info.name} • ID: {patientData.patid}
|
|
</Text>
|
|
<TouchableOpacity
|
|
style={styles.patientDetailButton}
|
|
onPress={handleBackToPatientDetails}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View style={styles.patientDetailButtonContent}>
|
|
<Icon name="user" size={14} color={theme.colors.primary} />
|
|
<Text style={styles.patientDetailButtonText}>View Details</Text>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</View>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Render Feedback History
|
|
*
|
|
* Purpose: Render feedback history display only
|
|
*/
|
|
const renderFeedbackHistory = () => {
|
|
const seriesFeedbacks = getSeriesFeedback();
|
|
|
|
return (
|
|
<View style={styles.feedbackHistory}>
|
|
{/* Feedback History */}
|
|
<View style={styles.feedbackList}>
|
|
<Text style={styles.feedbackListTitle}>
|
|
Feedback History ({seriesFeedbacks.length})
|
|
</Text>
|
|
|
|
{seriesFeedbacks.length === 0 ? (
|
|
<View style={styles.emptyFeedbackState}>
|
|
<Icon name="message-circle" size={48} color={theme.colors.textMuted} />
|
|
<Text style={styles.emptyFeedbackTitle}>No Feedback Yet</Text>
|
|
<Text style={styles.emptyFeedbackSubtitle}>
|
|
No feedback has been provided for this series yet
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
seriesFeedbacks.map((feedback: Feedback) => (
|
|
<View key={feedback.feedback_id} style={styles.feedbackCard}>
|
|
<View style={styles.feedbackCardHeader}>
|
|
<View style={styles.feedbackCardHeaderLeft}>
|
|
<View style={[
|
|
styles.feedbackTypeIndicator,
|
|
{ backgroundColor: feedback.is_positive ? theme.colors.success : theme.colors.error }
|
|
]}>
|
|
<Icon
|
|
name={feedback.is_positive ? "thumbs-up" : "thumbs-down"}
|
|
size={12}
|
|
color={theme.colors.background}
|
|
/>
|
|
</View>
|
|
<Text style={styles.feedbackEmail}>{feedback.email}</Text>
|
|
{isFeedbackNew(feedback.feedback_id) && (
|
|
<View style={styles.newFeedbackBadge}>
|
|
<Text style={styles.newFeedbackBadgeText}>NEW</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
<Text style={styles.feedbackTimestamp}>
|
|
{new Date(feedback.created_at).toLocaleDateString()}
|
|
</Text>
|
|
</View>
|
|
|
|
<Text style={styles.feedbackText}>{feedback.feedback_text}</Text>
|
|
|
|
<View style={styles.feedbackCardFooter}>
|
|
<View style={[
|
|
styles.feedbackTypeBadge,
|
|
{ backgroundColor: getFeedbackTypeColor(feedback.feedback_type) }
|
|
]}>
|
|
<Text style={styles.feedbackTypeBadgeText}>
|
|
{feedback.feedback_type.replace('_', ' ').toUpperCase()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
))
|
|
)}
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// MAIN RENDER
|
|
// ============================================================================
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
|
|
{/* Fixed Series Header */}
|
|
{renderSeriesHeader()}
|
|
|
|
|
|
|
|
{/* Scrollable Feedback Content */}
|
|
<View style={styles.scrollableContent}>
|
|
<ScrollView
|
|
style={styles.content}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={isRefreshing}
|
|
onRefresh={handleRefresh}
|
|
colors={[theme.colors.primary]}
|
|
tintColor={theme.colors.primary}
|
|
/>
|
|
}
|
|
>
|
|
{/* Feedback History from Navigation Parameters */}
|
|
{renderFeedbackHistory()}
|
|
</ScrollView>
|
|
</View>
|
|
</SafeAreaView>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// 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.
|
|
*/
|