crearted UI and integrated Api for Physician suggestion
This commit is contained in:
parent
e8492cd442
commit
80a1688e19
@ -12,7 +12,7 @@ import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme';
|
||||
|
||||
// Import screens
|
||||
import { AIPredictionsScreen } from '../screens';
|
||||
import { AIPredictionsScreen, AIPredictionDetailScreen } from '../screens';
|
||||
import { ComingSoonScreen } from '../../../shared/components';
|
||||
|
||||
// Import types
|
||||
@ -152,20 +152,20 @@ const AIPredictionStackNavigator: React.FC = () => {
|
||||
{/* AI Prediction Details Screen */}
|
||||
<Stack.Screen
|
||||
name="AIPredictionDetails"
|
||||
component={ComingSoonScreen}
|
||||
component={AIPredictionDetailScreen}
|
||||
options={({ navigation, route }) => ({
|
||||
title: 'Prediction Details',
|
||||
title: 'Create Suggestion',
|
||||
headerLeft: () => (
|
||||
<HeaderBackButton onPress={() => navigation.goBack()} />
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderActionButton
|
||||
iconName="share-2"
|
||||
iconName="help-circle"
|
||||
onPress={() => {
|
||||
// Share prediction details
|
||||
console.log('Share prediction:', route.params?.caseId);
|
||||
// Show help for suggestion form
|
||||
console.log('Show help for case:', route.params?.caseId);
|
||||
}}
|
||||
accessibilityLabel="Share prediction"
|
||||
accessibilityLabel="Help"
|
||||
/>
|
||||
),
|
||||
})}
|
||||
|
||||
@ -19,7 +19,7 @@ import type { RouteProp } from '@react-navigation/native';
|
||||
*
|
||||
* Screens:
|
||||
* - AIPredictionList: Main list of AI predictions
|
||||
* - AIPredictionDetails: Detailed view of a specific prediction
|
||||
* - AIPredictionDetails: Detailed view of a specific prediction with suggestion form
|
||||
* - AIPredictionFilters: Advanced filtering options
|
||||
* - AIPredictionStats: Detailed statistics view
|
||||
*/
|
||||
|
||||
987
app/modules/AIPrediction/screens/AIPredictionDetailScreen.tsx
Normal file
987
app/modules/AIPrediction/screens/AIPredictionDetailScreen.tsx
Normal file
@ -0,0 +1,987 @@
|
||||
/*
|
||||
* File: AIPredictionDetailScreen.tsx
|
||||
* Description: AI Prediction detail screen with suggestion form for updating AI findings
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Alert,
|
||||
SafeAreaView,
|
||||
StatusBar,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import Icon from 'react-native-vector-icons/Feather';
|
||||
import { theme } from '../../../theme';
|
||||
import { useAppDispatch, useAppSelector } from '../../../store/hooks';
|
||||
import { CustomModal } from '../../../shared/components/CustomModal';
|
||||
import Toast from 'react-native-toast-message';
|
||||
import { aiPredictionAPI } from '../services/aiPredictionAPI';
|
||||
import { showError } from '../../../shared/utils/toast';
|
||||
|
||||
// Import Redux selectors
|
||||
import { selectCurrentCase, selectIsLoadingCaseDetails, selectError } from '../redux';
|
||||
import { selectUser } from '../../Auth/redux/authSelectors';
|
||||
|
||||
// Import types
|
||||
import type { AIPredictionDetailsScreenProps } from '../navigation/navigationTypes';
|
||||
import type { AIPredictionCase } from '../types';
|
||||
|
||||
// ============================================================================
|
||||
// ENUMS & TYPES
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Suggestion Type Enum
|
||||
*
|
||||
* Purpose: Define available suggestion types as specified by user
|
||||
*/
|
||||
export enum SuggestionType {
|
||||
TREATMENT = 'Treatment',
|
||||
FOLLOW_UP = 'Follow Up',
|
||||
DIAGNOSIS = 'Diagnosis',
|
||||
OTHER = 'Other'
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority Enum
|
||||
*
|
||||
* Purpose: Define priority levels as specified by user
|
||||
*/
|
||||
export enum Priority {
|
||||
LOW = 'Low',
|
||||
MEDIUM = 'Medium',
|
||||
HIGH = 'High',
|
||||
CRITICAL = 'Critical'
|
||||
}
|
||||
|
||||
/**
|
||||
* Related Finding Interface
|
||||
*
|
||||
* Purpose: Structure for key-value pairs in related findings
|
||||
*/
|
||||
interface RelatedFinding {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Suggestion Form Data Interface
|
||||
*
|
||||
* Purpose: Structure for suggestion form data
|
||||
*/
|
||||
interface SuggestionFormData {
|
||||
patientId: string;
|
||||
suggestionType: SuggestionType;
|
||||
title: string;
|
||||
suggestionText: string;
|
||||
confidence: string;
|
||||
priority: Priority;
|
||||
category: string;
|
||||
costEstimate: string;
|
||||
timeEstimate: string;
|
||||
expiresAt: Date | null;
|
||||
aiModelVersion: string;
|
||||
evidenceSources: string;
|
||||
contraindications: string;
|
||||
tags: string;
|
||||
relatedFindings: RelatedFinding[];
|
||||
}
|
||||
|
||||
/**
|
||||
* API Response Interface
|
||||
*
|
||||
* Purpose: Structure for API response data
|
||||
*/
|
||||
interface APIResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data: {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
status: string;
|
||||
suggestion_id: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const SUGGESTION_TYPE_OPTIONS = [
|
||||
{ label: 'Treatment', value: SuggestionType.TREATMENT },
|
||||
{ label: 'Follow Up', value: SuggestionType.FOLLOW_UP },
|
||||
{ label: 'Diagnosis', value: SuggestionType.DIAGNOSIS },
|
||||
{ label: 'Other', value: SuggestionType.OTHER },
|
||||
];
|
||||
|
||||
|
||||
|
||||
const PRIORITY_OPTIONS = [
|
||||
{ label: 'Low', value: Priority.LOW, color: theme.colors.success },
|
||||
{ label: 'Medium', value: Priority.MEDIUM, color: theme.colors.warning },
|
||||
{ label: 'High', value: Priority.HIGH, color: '#FF5722' },
|
||||
{ label: 'Critical', value: Priority.CRITICAL, color: theme.colors.critical },
|
||||
];
|
||||
|
||||
// ============================================================================
|
||||
// AI PREDICTION DETAIL SCREEN COMPONENT
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* AIPredictionDetailScreen Component
|
||||
*
|
||||
* Purpose: Display AI prediction details and allow suggestion updates
|
||||
*
|
||||
* Features:
|
||||
* - View complete AI prediction details
|
||||
* - Create/update suggestions for AI findings
|
||||
* - Form validation and submission
|
||||
* - Dynamic related findings management
|
||||
* - Date picker for expiration
|
||||
* - Dropdown selections for types and priorities
|
||||
* - Responsive design with keyboard handling
|
||||
*/
|
||||
const AIPredictionDetailScreen: React.FC<AIPredictionDetailsScreenProps> = ({
|
||||
navigation,
|
||||
route
|
||||
}) => {
|
||||
// ============================================================================
|
||||
// REDUX STATE
|
||||
// ============================================================================
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const user = useAppSelector(selectUser);
|
||||
const currentCase = useAppSelector(selectCurrentCase);
|
||||
const isLoading = useAppSelector(selectIsLoadingCaseDetails);
|
||||
const error = useAppSelector(selectError);
|
||||
|
||||
// ============================================================================
|
||||
// LOCAL STATE
|
||||
// ============================================================================
|
||||
|
||||
const [formData, setFormData] = useState<SuggestionFormData>({
|
||||
patientId: route.params.caseId,
|
||||
suggestionType: SuggestionType.TREATMENT,
|
||||
title: '',
|
||||
suggestionText: '',
|
||||
confidence: '0.99',
|
||||
priority: Priority.HIGH,
|
||||
category: 'Radiology',
|
||||
costEstimate: '',
|
||||
timeEstimate: '1-2 hours',
|
||||
expiresAt: null,
|
||||
aiModelVersion: 'v2.1.0',
|
||||
evidenceSources: '',
|
||||
contraindications: '',
|
||||
tags: '',
|
||||
relatedFindings: [
|
||||
{ key: 'finding_label', value: 'midline shift' },
|
||||
{ key: 'finding_type', value: 'no_pathology' },
|
||||
{ key: 'clinical_urgency', value: 'urgent' },
|
||||
{ key: 'primary_severity', value: 'high' },
|
||||
{ key: 'modality', value: '/DX' },
|
||||
{ key: 'institution', value: 'Brighton Radiology' },
|
||||
],
|
||||
});
|
||||
|
||||
const [showSuggestionTypeDropdown, setShowSuggestionTypeDropdown] = useState(false);
|
||||
const [showPriorityDropdown, setShowPriorityDropdown] = useState(false);
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
||||
const [apiResponse, setApiResponse] = useState<any>(null);
|
||||
|
||||
// ============================================================================
|
||||
// EFFECTS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Initialize form data from current case
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (currentCase) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
patientId: currentCase.patid,
|
||||
title: `Recommended CT Scan`,
|
||||
suggestionText: `Describe your suggestion with clinical reasoning...`,
|
||||
confidence: currentCase.prediction.confidence_score.toFixed(4),
|
||||
}));
|
||||
}
|
||||
}, [currentCase]);
|
||||
|
||||
// ============================================================================
|
||||
// EVENT HANDLERS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Handle form field changes
|
||||
*/
|
||||
const handleFieldChange = useCallback((field: keyof SuggestionFormData, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle dropdown selection
|
||||
*/
|
||||
const handleDropdownSelect = useCallback((field: keyof SuggestionFormData, value: any) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
setShowSuggestionTypeDropdown(false);
|
||||
setShowPriorityDropdown(false);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle date selection
|
||||
*/
|
||||
const handleDateChange = useCallback((event: any, selectedDate?: Date) => {
|
||||
setShowDatePicker(false);
|
||||
if (selectedDate) {
|
||||
handleFieldChange('expiresAt', selectedDate);
|
||||
}
|
||||
}, [handleFieldChange]);
|
||||
|
||||
/**
|
||||
* Add new related finding
|
||||
*/
|
||||
const handleAddRelatedFinding = useCallback(() => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
relatedFindings: [...prev.relatedFindings, { key: '', value: '' }],
|
||||
}));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Remove related finding
|
||||
*/
|
||||
const handleRemoveRelatedFinding = useCallback((index: number) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
relatedFindings: prev.relatedFindings.filter((_, i) => i !== index),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Update related finding
|
||||
*/
|
||||
const handleUpdateRelatedFinding = useCallback((index: number, field: 'key' | 'value', value: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
relatedFindings: prev.relatedFindings.map((finding, i) =>
|
||||
i === index ? { ...finding, [field]: value } : finding
|
||||
),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Validate form data
|
||||
*/
|
||||
const validateForm = useCallback((): boolean => {
|
||||
if (!formData.title.trim()) {
|
||||
showError('Validation Error', 'Title is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.suggestionText.trim()) {
|
||||
showError('Validation Error', 'Suggestion text is required');
|
||||
return false;
|
||||
}
|
||||
if (!formData.patientId.trim()) {
|
||||
showError('Validation Error', 'Patient ID is required');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate expiry date - must be in the future
|
||||
if (formData.expiresAt) {
|
||||
const now = new Date();
|
||||
const expiryDate = new Date(formData.expiresAt);
|
||||
|
||||
if (expiryDate <= now) {
|
||||
showError('Validation Error', 'Expiry date must be in the future');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [formData]);
|
||||
|
||||
/**
|
||||
* Handle form submission
|
||||
*/
|
||||
const handleSubmitSuggestion = useCallback(async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
// Prepare API payload according to backend structure
|
||||
const apiPayload = {
|
||||
patid: formData.patientId,
|
||||
suggestion_type: formData.suggestionType.toLowerCase().replace(' ', '_'),
|
||||
suggestion_title: formData.title,
|
||||
suggestion_text: formData.suggestionText,
|
||||
confidence_score: parseFloat(formData.confidence),
|
||||
priority_level: formData.priority.toLowerCase(),
|
||||
category: formData.category,
|
||||
related_findings: formData.relatedFindings.reduce((acc, finding) => {
|
||||
if (finding.key && finding.value) {
|
||||
acc[finding.key] = finding.value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>),
|
||||
evidence_sources: formData.evidenceSources ? formData.evidenceSources.split(',').map(s => s.trim()) : [],
|
||||
contraindications: formData.contraindications || 'none',
|
||||
cost_estimate: formData.costEstimate ? parseFloat(formData.costEstimate) : 0,
|
||||
time_estimate: formData.timeEstimate,
|
||||
expires_at: formData.expiresAt ? formData.expiresAt.toISOString() : null,
|
||||
tags: formData.tags ? formData.tags.split(',').map(s => s.trim()) : [],
|
||||
ai_model_version: formData.aiModelVersion,
|
||||
};
|
||||
|
||||
// Call centralized API service
|
||||
const response = await aiPredictionAPI.submitAISuggestion(apiPayload, user?.access_token || '');
|
||||
console.log('feed back response',response)
|
||||
if (response.ok && response.data) {
|
||||
const responseData = response.data as APIResponse;
|
||||
|
||||
if (responseData.success) {
|
||||
// Show success toast
|
||||
Toast.show({
|
||||
type: 'success',
|
||||
text1: 'Success!',
|
||||
text2: responseData.message || 'Suggestion submitted successfully',
|
||||
position: 'top',
|
||||
visibilityTime: 4000,
|
||||
});
|
||||
|
||||
// Store API response data
|
||||
setApiResponse(responseData.data);
|
||||
// Show success modal with API response data
|
||||
setShowSuccessModal(true);
|
||||
} else {
|
||||
throw new Error(responseData.message || 'Failed to submit suggestion');
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.problem || 'HTTP request failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submission Error:', error);
|
||||
|
||||
// Show error toast
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Error',
|
||||
text2: error instanceof Error ? error.message : 'Failed to submit suggestion. Please try again.',
|
||||
position: 'top',
|
||||
visibilityTime: 5000,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}, [formData, validateForm, user]);
|
||||
|
||||
/**
|
||||
* Handle success modal close
|
||||
*/
|
||||
const handleSuccessModalClose = useCallback(() => {
|
||||
setShowSuccessModal(false);
|
||||
navigation.goBack();
|
||||
}, [navigation]);
|
||||
|
||||
/**
|
||||
* Handle back navigation
|
||||
*/
|
||||
const handleGoBack = useCallback(() => {
|
||||
navigation.goBack();
|
||||
}, [navigation]);
|
||||
|
||||
// ============================================================================
|
||||
// RENDER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Render dropdown options
|
||||
*/
|
||||
const renderDropdownOptions = useCallback((
|
||||
options: Array<{ label: string; value: any; color?: string }>,
|
||||
currentValue: any,
|
||||
onSelect: (value: any) => void
|
||||
) => (
|
||||
<View style={styles.dropdownOptions}>
|
||||
{options.map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option.value}
|
||||
style={[
|
||||
styles.dropdownOption,
|
||||
currentValue === option.value && styles.dropdownOptionSelected
|
||||
]}
|
||||
onPress={() => onSelect(option.value)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.dropdownOptionText,
|
||||
currentValue === option.value && styles.dropdownOptionTextSelected,
|
||||
option.color && { color: option.color }
|
||||
]}>
|
||||
{option.label}
|
||||
</Text>
|
||||
{currentValue === option.value && (
|
||||
<Icon name="check" size={16} color={theme.colors.primary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
), []);
|
||||
|
||||
/**
|
||||
* Render related findings section
|
||||
*/
|
||||
const renderRelatedFindings = useCallback(() => (
|
||||
<View style={styles.relatedFindingsContainer}>
|
||||
<Text style={styles.sectionTitle}>Related Findings</Text>
|
||||
{formData.relatedFindings.map((finding, index) => (
|
||||
<View key={index} style={styles.relatedFindingItem}>
|
||||
<TextInput
|
||||
style={[styles.input, styles.relatedFindingKey]}
|
||||
value={finding.key}
|
||||
onChangeText={(value) => handleUpdateRelatedFinding(index, 'key', value)}
|
||||
placeholder="Key"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
/>
|
||||
<TextInput
|
||||
style={[styles.input, styles.relatedFindingValue]}
|
||||
value={finding.value}
|
||||
onChangeText={(value) => handleUpdateRelatedFinding(index, 'value', value)}
|
||||
placeholder="Value"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.removeButton}
|
||||
onPress={() => handleRemoveRelatedFinding(index)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Remove related finding"
|
||||
>
|
||||
<Icon name="trash-2" size={18} color={theme.colors.error} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
style={styles.addButton}
|
||||
onPress={handleAddRelatedFinding}
|
||||
>
|
||||
<Text style={styles.addButtonText}>Add</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
), [formData.relatedFindings, handleUpdateRelatedFinding, handleRemoveRelatedFinding, handleAddRelatedFinding]);
|
||||
|
||||
// ============================================================================
|
||||
// RENDER
|
||||
// ============================================================================
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<StatusBar barStyle="dark-content" backgroundColor={theme.colors.background} />
|
||||
|
||||
{/* Header */}
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={styles.keyboardAvoidingView}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 0}
|
||||
enabled={Platform.OS === 'ios'}
|
||||
>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* Patient ID */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Patient ID</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.disabledInput]}
|
||||
value={formData.patientId}
|
||||
editable={false}
|
||||
placeholder="Patient ID"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Suggestion Type */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Suggestion Type</Text>
|
||||
<View style={styles.dropdownContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.dropdown}
|
||||
onPress={() => setShowSuggestionTypeDropdown(!showSuggestionTypeDropdown)}
|
||||
>
|
||||
<Text style={styles.dropdownText}>{formData.suggestionType}</Text>
|
||||
<Icon name="chevron-down" size={20} color={theme.colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
{showSuggestionTypeDropdown && renderDropdownOptions(
|
||||
SUGGESTION_TYPE_OPTIONS,
|
||||
formData.suggestionType,
|
||||
(value) => handleDropdownSelect('suggestionType', value)
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Title */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Title</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={formData.title}
|
||||
onChangeText={(value) => handleFieldChange('title', value)}
|
||||
placeholder="Recommended CT Scan"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Suggestion Text */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Suggestion Text</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={formData.suggestionText}
|
||||
onChangeText={(value) => handleFieldChange('suggestionText', value)}
|
||||
placeholder="Describe your suggestion with clinical reasoning..."
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
multiline
|
||||
numberOfLines={4}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Row 1: Confidence and Priority */}
|
||||
<View style={styles.formRow}>
|
||||
<View style={[styles.formGroup, styles.formGroupHalf]}>
|
||||
<Text style={styles.label}>Confidence (0-1)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={formData.confidence}
|
||||
onChangeText={(value) => handleFieldChange('confidence', value)}
|
||||
placeholder="0.9979"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[styles.formGroup, styles.formGroupHalf]}>
|
||||
<Text style={styles.label}>Priority</Text>
|
||||
<View style={styles.dropdownContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.dropdown}
|
||||
onPress={() => setShowPriorityDropdown(!showPriorityDropdown)}
|
||||
>
|
||||
<Text style={styles.dropdownText}>{formData.priority}</Text>
|
||||
<Icon name="chevron-down" size={20} color={theme.colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
{showPriorityDropdown && renderDropdownOptions(
|
||||
PRIORITY_OPTIONS,
|
||||
formData.priority,
|
||||
(value) => handleDropdownSelect('priority', value)
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Row 2: Category and Cost Estimate */}
|
||||
<View style={styles.formRow}>
|
||||
<View style={[styles.formGroup, styles.formGroupHalf]}>
|
||||
<Text style={styles.label}>Category</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={formData.category}
|
||||
onChangeText={(value) => handleFieldChange('category', value)}
|
||||
placeholder="Radiology"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[styles.formGroup, styles.formGroupHalf]}>
|
||||
<Text style={styles.label}>Cost Estimate (USD)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={formData.costEstimate}
|
||||
onChangeText={(value) => handleFieldChange('costEstimate', value)}
|
||||
placeholder="Enter cost"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Row 3: Time Estimate and Expires At */}
|
||||
<View style={styles.formRow}>
|
||||
<View style={[styles.formGroup, styles.formGroupHalf]}>
|
||||
<Text style={styles.label}>Time Estimate</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={formData.timeEstimate}
|
||||
onChangeText={(value) => handleFieldChange('timeEstimate', value)}
|
||||
placeholder="1-2 hours"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={[styles.formGroup, styles.formGroupHalf]}>
|
||||
<Text style={styles.label}>Expires At</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.dateInput}
|
||||
onPress={() => setShowDatePicker(true)}
|
||||
>
|
||||
<Text style={[
|
||||
styles.dateText,
|
||||
!formData.expiresAt && styles.placeholderText
|
||||
]}>
|
||||
{formData.expiresAt
|
||||
? formData.expiresAt.toLocaleDateString()
|
||||
: 'Select date'
|
||||
}
|
||||
</Text>
|
||||
<Icon name="calendar" size={20} color={theme.colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.helperText}>Date must be in the future</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* AI Model Version */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>AI Model Version</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={formData.aiModelVersion}
|
||||
onChangeText={(value) => handleFieldChange('aiModelVersion', value)}
|
||||
placeholder="v2.1.0"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Evidence Sources */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Evidence Sources (comma-separated)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={formData.evidenceSources}
|
||||
onChangeText={(value) => handleFieldChange('evidenceSources', value)}
|
||||
placeholder="Evidence A, Protocol B"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Contraindications */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Contraindications</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={formData.contraindications}
|
||||
onChangeText={(value) => handleFieldChange('contraindications', value)}
|
||||
placeholder="Any known contraindications..."
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Tags */}
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.label}>Tags (comma-separated)</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={formData.tags}
|
||||
onChangeText={(value) => handleFieldChange('tags', value)}
|
||||
placeholder="emergency, chest, pulmonary"
|
||||
placeholderTextColor={theme.colors.textMuted}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Related Findings */}
|
||||
{renderRelatedFindings()}
|
||||
|
||||
{/* Submit Button */}
|
||||
<TouchableOpacity
|
||||
style={[styles.submitButton, isSubmitting && styles.submitButtonDisabled]}
|
||||
onPress={handleSubmitSuggestion}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Text style={styles.submitButtonText}>Submitting...</Text>
|
||||
) : (
|
||||
<Text style={styles.submitButtonText}>Submit Suggestion</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
{/* Date Picker */}
|
||||
{showDatePicker && (
|
||||
<DateTimePicker
|
||||
value={formData.expiresAt || new Date()}
|
||||
mode="date"
|
||||
display="default"
|
||||
onChange={handleDateChange}
|
||||
minimumDate={new Date()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Success Modal */}
|
||||
<CustomModal
|
||||
visible={showSuccessModal}
|
||||
title="Suggestion Submitted Successfully!"
|
||||
message={
|
||||
apiResponse ?
|
||||
`Your suggestion has been submitted and will be reviewed by the medical team.\n\n` +
|
||||
`Suggestion ID: ${apiResponse.suggestion_id}\n` +
|
||||
`Status: ${apiResponse.status}\n` +
|
||||
`Created: ${new Date(apiResponse.created_at).toLocaleString()}`
|
||||
:
|
||||
"Your suggestion has been successfully submitted and will be reviewed by the medical team."
|
||||
}
|
||||
type="success"
|
||||
confirmText="OK"
|
||||
onConfirm={handleSuccessModalClose}
|
||||
onClose={handleSuccessModalClose}
|
||||
/>
|
||||
|
||||
{/* Toast Messages */}
|
||||
<Toast />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// STYLES
|
||||
// ============================================================================
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
backButton: {
|
||||
padding: theme.spacing.sm,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: theme.typography.fontSize.displaySmall,
|
||||
fontFamily: theme.typography.fontFamily.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
headerSpacer: {
|
||||
width: 40,
|
||||
},
|
||||
keyboardAvoidingView: {
|
||||
flex: 1,
|
||||
// Ensure this doesn't interfere with bottom tab navigation
|
||||
position: 'relative',
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: theme.spacing.md,
|
||||
paddingBottom: theme.spacing.xl,
|
||||
},
|
||||
formGroup: {
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
formGroupHalf: {
|
||||
flex: 1,
|
||||
},
|
||||
formRow: {
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing.md,
|
||||
},
|
||||
label: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontFamily: theme.typography.fontFamily.medium,
|
||||
color: theme.colors.textPrimary,
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
color: theme.colors.textPrimary,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
disabledInput: {
|
||||
backgroundColor: theme.colors.backgroundAlt,
|
||||
color: theme.colors.textSecondary,
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
textArea: {
|
||||
height: 80,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
dropdownContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
dropdown: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
dropdownText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
dropdownOptions: {
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
backgroundColor: theme.colors.background,
|
||||
marginTop: theme.spacing.xs,
|
||||
...theme.shadows.medium,
|
||||
},
|
||||
dropdownOption: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
dropdownOptionSelected: {
|
||||
backgroundColor: theme.colors.tertiary,
|
||||
},
|
||||
dropdownOptionText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
dropdownOptionTextSelected: {
|
||||
color: theme.colors.primary,
|
||||
fontFamily: theme.typography.fontFamily.medium,
|
||||
},
|
||||
dateInput: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
dateText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
color: theme.colors.textPrimary,
|
||||
},
|
||||
placeholderText: {
|
||||
color: theme.colors.textMuted,
|
||||
},
|
||||
helperText: {
|
||||
fontSize: theme.typography.fontSize.bodySmall,
|
||||
fontFamily: theme.typography.fontFamily.regular,
|
||||
color: theme.colors.textMuted,
|
||||
marginTop: theme.spacing.xs,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: theme.typography.fontSize.bodyLarge,
|
||||
fontFamily: theme.typography.fontFamily.bold,
|
||||
color: theme.colors.textPrimary,
|
||||
marginBottom: theme.spacing.md,
|
||||
},
|
||||
relatedFindingsContainer: {
|
||||
marginBottom: theme.spacing.lg,
|
||||
},
|
||||
relatedFindingItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: theme.spacing.sm,
|
||||
marginBottom: theme.spacing.sm,
|
||||
},
|
||||
relatedFindingKey: {
|
||||
flex: 1,
|
||||
},
|
||||
relatedFindingValue: {
|
||||
flex: 1,
|
||||
},
|
||||
addButton: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
paddingHorizontal: theme.spacing.md,
|
||||
paddingVertical: theme.spacing.sm,
|
||||
alignItems: 'center',
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
addButtonText: {
|
||||
fontSize: theme.typography.fontSize.bodyMedium,
|
||||
fontFamily: theme.typography.fontFamily.medium,
|
||||
color: theme.colors.background,
|
||||
},
|
||||
removeButton: {
|
||||
padding: theme.spacing.xs,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: theme.colors.primary,
|
||||
borderRadius: theme.borderRadius.medium,
|
||||
paddingVertical: theme.spacing.md,
|
||||
alignItems: 'center',
|
||||
marginTop: theme.spacing.lg,
|
||||
...theme.shadows.medium,
|
||||
},
|
||||
submitButtonDisabled: {
|
||||
backgroundColor: theme.colors.textMuted,
|
||||
},
|
||||
submitButtonText: {
|
||||
fontSize: theme.typography.fontSize.bodyLarge,
|
||||
fontFamily: theme.typography.fontFamily.bold,
|
||||
color: theme.colors.background,
|
||||
},
|
||||
});
|
||||
|
||||
export default AIPredictionDetailScreen;
|
||||
|
||||
/*
|
||||
* End of File: AIPredictionDetailScreen.tsx
|
||||
* Design & Developed by Tech4Biz Solutions
|
||||
* Copyright (c) Spurrin Innovations. All rights reserved.
|
||||
*/
|
||||
@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
export { default as AIPredictionsScreen } from './AIPredictionsScreen';
|
||||
export { default as AIPredictionDetailScreen } from './AIPredictionDetailScreen';
|
||||
|
||||
/*
|
||||
* End of File: index.ts
|
||||
|
||||
@ -223,6 +223,110 @@ export const aiPredictionAPI = {
|
||||
improvement_suggestions?: string;
|
||||
}, token: string) => {
|
||||
return api.post(`/api/ai-cases/feedback/${caseId}`, feedbackData, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Submit AI Suggestion
|
||||
*
|
||||
* Purpose: Submit physician suggestions for AI findings
|
||||
*
|
||||
* @param suggestionData - Suggestion data including patient ID, type, title, text, etc.
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with suggestion submission result
|
||||
*/
|
||||
submitAISuggestion: (suggestionData: {
|
||||
patid: string;
|
||||
suggestion_type: string;
|
||||
suggestion_title: string;
|
||||
suggestion_text: string;
|
||||
confidence_score: number;
|
||||
priority_level: string;
|
||||
category: string;
|
||||
related_findings: Record<string, string>;
|
||||
evidence_sources: string[];
|
||||
contraindications: string;
|
||||
cost_estimate: number;
|
||||
time_estimate: string;
|
||||
expires_at: string | null;
|
||||
tags: string[];
|
||||
ai_model_version: string;
|
||||
}, token: string) => {
|
||||
return api.post('/api/ai-cases/suggestions', suggestionData, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get AI Suggestions
|
||||
*
|
||||
* Purpose: Fetch AI suggestions for a specific case or all suggestions
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @param params - Optional query parameters
|
||||
* @returns Promise with suggestions data
|
||||
*/
|
||||
getAISuggestions: (token: string, params?: {
|
||||
caseId?: string;
|
||||
patientId?: string;
|
||||
suggestionType?: string;
|
||||
priority?: string;
|
||||
status?: string;
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}) => {
|
||||
const queryParams = params ? { ...params } : {};
|
||||
return api.get('/api/ai-cases/suggestions', queryParams, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update AI Suggestion
|
||||
*
|
||||
* Purpose: Update an existing AI suggestion
|
||||
*
|
||||
* @param suggestionId - Suggestion ID to update
|
||||
* @param updateData - Updated suggestion data
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with updated suggestion data
|
||||
*/
|
||||
updateAISuggestion: (suggestionId: string, updateData: {
|
||||
suggestion_title?: string;
|
||||
suggestion_text?: string;
|
||||
priority_level?: string;
|
||||
category?: string;
|
||||
related_findings?: Record<string, string>;
|
||||
evidence_sources?: string[];
|
||||
contraindications?: string;
|
||||
cost_estimate?: number;
|
||||
time_estimate?: string;
|
||||
expires_at?: string | null;
|
||||
tags?: string[];
|
||||
}, token: string) => {
|
||||
return api.put(`/api/ai-cases/suggestions/${suggestionId}`, updateData, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete AI Suggestion
|
||||
*
|
||||
* Purpose: Delete an AI suggestion
|
||||
*
|
||||
* @param suggestionId - Suggestion ID to delete
|
||||
* @param token - Authentication token
|
||||
* @returns Promise with deletion result
|
||||
*/
|
||||
deleteAISuggestion: (suggestionId: string, token: string) => {
|
||||
return api.delete(`/api/ai-cases/suggestions/${suggestionId}`, {}, buildHeaders({ token }));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get Suggestion Statistics
|
||||
*
|
||||
* Purpose: Fetch statistics about AI suggestions
|
||||
*
|
||||
* @param token - Authentication token
|
||||
* @param timeRange - Optional time range filter
|
||||
* @returns Promise with suggestion statistics
|
||||
*/
|
||||
getSuggestionStats: (token: string, timeRange?: 'today' | 'week' | 'month') => {
|
||||
const params = timeRange ? { timeRange } : {};
|
||||
return api.get('/api/ai-cases/suggestions/statistics', params, buildHeaders({ token }));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ export const MainTabNavigator: React.FC = () => {
|
||||
// Tab bar styling for active and inactive states
|
||||
tabBarActiveTintColor: theme.colors.primary, // Blue color for active tab
|
||||
tabBarInactiveTintColor: theme.colors.textMuted, // Gray color for inactive tab
|
||||
tabBarHideOnKeyboard: true,
|
||||
|
||||
// Tab bar container styling
|
||||
tabBarStyle: {
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^2.1.0",
|
||||
"@react-native-clipboard/clipboard": "^1.16.1",
|
||||
"@react-native-community/datetimepicker": "^8.4.4",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
@ -3207,6 +3208,29 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/datetimepicker": {
|
||||
"version": "8.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-8.4.4.tgz",
|
||||
"integrity": "sha512-bc4ZixEHxZC9/qf5gbdYvIJiLZ5CLmEsC3j+Yhe1D1KC/3QhaIfGDVdUcid0PdlSoGOSEq4VlB93AWyetEyBSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": ">=52.0.0",
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-windows": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"expo": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native-windows": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/netinfo": {
|
||||
"version": "11.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.4.1.tgz",
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "^2.1.0",
|
||||
"@react-native-clipboard/clipboard": "^1.16.1",
|
||||
"@react-native-community/datetimepicker": "^8.4.4",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-navigation/bottom-tabs": "^7.2.0",
|
||||
"@react-navigation/native": "^7.0.14",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user