NeoScan_Radiologist/app/modules/Auth/components/signup/HospitalSelectionStep.tsx
2025-08-05 18:01:36 +05:30

592 lines
15 KiB
TypeScript

/*
* File: HospitalSelectionStep.tsx
* Description: Hospital selection step component for signup flow
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/
import React, { useState, useMemo } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
ScrollView,
FlatList,
ActivityIndicator,
TextInput,
KeyboardAvoidingView,
Platform,
} from 'react-native';
import { theme } from '../../../../theme/theme';
import { HospitalSelectionStepProps, Hospital } from '../../types/signup';
import Icon from 'react-native-vector-icons/Feather';
// ============================================================================
// HOSPITAL SELECTION STEP COMPONENT
// ============================================================================
/**
* HospitalSelectionStep Component
*
* Purpose: Fifth step of signup flow - hospital selection
*
* Features:
* - Hospital list display from Redux state
* - Hospital selection with visual feedback
* - Search functionality for hospitals
* - Loading states and error handling
* - Continue button with validation (sticky bottom)
* - Back navigation with modern header
* - Scrollable hospital list
*/
const HospitalSelectionStep: React.FC<HospitalSelectionStepProps> = ({
onContinue,
onBack,
data,
isLoading,
hospitals,
hospitalLoading,
}) => {
// ============================================================================
// STATE MANAGEMENT
// ============================================================================
const [selectedHospitalId, setSelectedHospitalId] = useState(data.hospital_id || '');
const [searchQuery, setSearchQuery] = useState('');
// ============================================================================
// COMPUTED VALUES
// ============================================================================
/**
* Filtered Hospitals
*
* Purpose: Filter hospitals based on search query
*/
const filteredHospitals = useMemo(() => {
if (!hospitals) return [];
if (!searchQuery.trim()) return hospitals;
const query = searchQuery.toLowerCase();
return hospitals.filter(hospital =>
hospital.hospital_name?.toLowerCase().includes(query)
);
}, [hospitals, searchQuery]);
// ============================================================================
// HANDLERS
// ============================================================================
/**
* Handle Hospital Selection
*
* Purpose: Select a hospital
*/
const handleHospitalSelect = (hospitalId: string) => {
setSelectedHospitalId(hospitalId);
};
/**
* Handle Continue
*
* Purpose: Validate selection and proceed to next step
*/
const handleContinue = () => {
if (!selectedHospitalId) {
// Show error or alert
return;
}
onContinue(selectedHospitalId);
};
/**
* Handle Search Input Change
*
* Purpose: Update search query
*/
const handleSearchChange = (text: string) => {
setSearchQuery(text);
};
/**
* Clear Search
*
* Purpose: Clear search query
*/
const handleClearSearch = () => {
setSearchQuery('');
};
// ============================================================================
// RENDER FUNCTIONS
// ============================================================================
/**
* Render Hospital Item
*
* Purpose: Render individual hospital item
*/
const renderHospitalItem = ({ item }: { item: Hospital }) => {
const isSelected = selectedHospitalId === item.hospital_id;
return (
<TouchableOpacity
style={[
styles.hospitalItem,
isSelected && styles.hospitalItemSelected,
]}
onPress={() => handleHospitalSelect(item.hospital_id || '')}
disabled={isLoading}
>
<View style={styles.hospitalContent}>
<View style={styles.hospitalInfo}>
<Text style={[
styles.hospitalName,
isSelected && styles.hospitalNameSelected,
]}>
{item.hospital_name || 'Unknown Hospital'}
</Text>
</View>
{isSelected && (
<View style={styles.selectedIcon}>
<Icon name="check" size={20} color={theme.colors.background} />
</View>
)}
</View>
</TouchableOpacity>
);
};
/**
* Render Search Input
*
* Purpose: Render search input field
*/
const renderSearchInput = () => (
<View style={styles.searchContainer}>
<View style={styles.searchInputWrapper}>
<Icon name="search" size={20} color={theme.colors.textMuted} style={styles.searchIcon} />
<TextInput
style={styles.searchInput}
placeholder="Search hospitals..."
placeholderTextColor={theme.colors.textMuted}
value={searchQuery}
onChangeText={handleSearchChange}
autoCapitalize="none"
autoCorrect={false}
editable={!hospitalLoading}
/>
{searchQuery.length > 0 && (
<TouchableOpacity onPress={handleClearSearch} style={styles.clearButton}>
<Icon name="x" size={18} color={theme.colors.textMuted} />
</TouchableOpacity>
)}
</View>
</View>
);
/**
* Render Loading State
*
* Purpose: Show loading indicator while fetching hospitals
*/
const renderLoadingState = () => (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={theme.colors.primary} />
<Text style={styles.loadingText}>Loading hospitals...</Text>
</View>
);
/**
* Render Empty State
*
* Purpose: Show message when no hospitals are available
*/
const renderEmptyState = () => (
<View style={styles.emptyContainer}>
<Icon name="building" size={48} color={theme.colors.textMuted} />
<Text style={styles.emptyTitle}>
{searchQuery ? 'No Hospitals Found' : 'No Hospitals Available'}
</Text>
<Text style={styles.emptyText}>
{searchQuery
? 'Try adjusting your search terms or browse all hospitals.'
: 'There are no hospitals available at the moment. Please try again later.'
}
</Text>
</View>
);
/**
* Render Search Results Info
*
* Purpose: Show search results count
*/
const renderSearchResultsInfo = () => {
if (!searchQuery.trim()) return null;
return (
<View style={styles.searchResultsInfo}>
<Text style={styles.searchResultsText}>
{filteredHospitals.length} hospital{filteredHospitals.length !== 1 ? 's' : ''} found
</Text>
</View>
);
};
// ============================================================================
// RENDER
// ============================================================================
return (
<View style={styles.container}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={onBack} style={styles.backButton}>
<Icon name="arrow-left" size={24} color={theme.colors.textPrimary} />
</TouchableOpacity>
<View style={styles.headerContent}>
<Text style={styles.title}>Create Account</Text>
<Text style={styles.subtitle}>Step 5 of 5</Text>
</View>
<View style={styles.headerSpacer} />
</View>
{/* Content */}
<View style={styles.content}>
<Text style={styles.sectionTitle}>Select Your Hospital</Text>
<Text style={styles.description}>
Choose the hospital where you work or will be practicing.
</Text>
{/* Search Input */}
{renderSearchInput()}
{/* Search Results Info */}
{renderSearchResultsInfo()}
{/* Hospital List */}
<View style={styles.hospitalListContainer}>
{hospitalLoading ? (
renderLoadingState()
) : filteredHospitals.length > 0 ? (
<FlatList
data={filteredHospitals}
renderItem={renderHospitalItem}
keyExtractor={(item) => item.hospital_id || ''}
showsVerticalScrollIndicator={true}
contentContainerStyle={styles.hospitalList}
keyboardShouldPersistTaps="handled"
/>
) : (
renderEmptyState()
)}
</View>
</View>
{/* Sticky Continue Button */}
<View style={styles.stickyButtonContainer}>
<TouchableOpacity
style={[
styles.continueButton,
(!selectedHospitalId || isLoading || hospitalLoading)
? styles.continueButtonDisabled
: null,
]}
onPress={handleContinue}
disabled={!selectedHospitalId || isLoading || hospitalLoading}
>
<Text style={[
styles.continueButtonText,
(!selectedHospitalId || isLoading || hospitalLoading)
? styles.continueButtonTextDisabled
: null,
]}>
{isLoading ? 'Creating Account...' : 'Create Account'}
</Text>
</TouchableOpacity>
</View>
</View>
);
};
// ============================================================================
// STYLES
// ============================================================================
const styles = StyleSheet.create({
// Main container
container: {
flex: 1,
backgroundColor: theme.colors.background,
},
// Header section
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: theme.spacing.xl,
paddingBottom: theme.spacing.lg,
paddingHorizontal: theme.spacing.lg,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
},
// Back button
backButton: {
padding: theme.spacing.sm,
borderRadius: theme.borderRadius.medium,
backgroundColor: theme.colors.backgroundAlt,
},
// Header content
headerContent: {
flex: 1,
alignItems: 'center',
},
// Header spacer
headerSpacer: {
width: 40,
},
// Title
title: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.xs,
},
// Subtitle
subtitle: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
},
// Content section
content: {
flex: 1,
paddingHorizontal: theme.spacing.lg,
paddingTop: theme.spacing.lg,
},
// Section title
sectionTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginBottom: theme.spacing.sm,
},
// Description
description: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginBottom: theme.spacing.lg,
},
// Search container
searchContainer: {
marginBottom: theme.spacing.lg,
},
// Search input wrapper
searchInputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: theme.colors.backgroundAlt,
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
paddingHorizontal: theme.spacing.md,
},
// Search icon
searchIcon: {
marginRight: theme.spacing.sm,
},
// Search input
searchInput: {
flex: 1,
paddingVertical: theme.spacing.md,
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textPrimary,
},
// Clear button
clearButton: {
padding: theme.spacing.xs,
marginLeft: theme.spacing.sm,
},
// Search results info
searchResultsInfo: {
marginBottom: theme.spacing.md,
},
// Search results text
searchResultsText: {
fontSize: theme.typography.fontSize.bodySmall,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textSecondary,
},
// Hospital list container
hospitalListContainer: {
flex: 1,
},
// Hospital list
hospitalList: {
paddingBottom: theme.spacing.lg,
},
// Hospital item
hospitalItem: {
backgroundColor: theme.colors.background,
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.medium,
padding: theme.spacing.md,
marginBottom: theme.spacing.sm,
},
// Hospital item selected
hospitalItemSelected: {
borderColor: theme.colors.primary,
backgroundColor: theme.colors.tertiary,
},
// Hospital content
hospitalContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
// Hospital info
hospitalInfo: {
flex: 1,
},
// Hospital name
hospitalName: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.medium,
color: theme.colors.textPrimary,
},
// Hospital name selected
hospitalNameSelected: {
color: theme.colors.primary,
fontFamily: theme.typography.fontFamily.bold,
},
// Selected icon
selectedIcon: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: theme.colors.primary,
alignItems: 'center',
justifyContent: 'center',
},
// Loading container
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: theme.spacing.xxl,
},
// Loading text
loadingText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
marginTop: theme.spacing.md,
},
// Empty container
emptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: theme.spacing.xxl,
},
// Empty title
emptyTitle: {
fontSize: theme.typography.fontSize.displaySmall,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.textPrimary,
marginTop: theme.spacing.md,
marginBottom: theme.spacing.sm,
},
// Empty text
emptyText: {
fontSize: theme.typography.fontSize.bodyMedium,
fontFamily: theme.typography.fontFamily.regular,
color: theme.colors.textSecondary,
textAlign: 'center',
paddingHorizontal: theme.spacing.lg,
},
// Sticky button container
stickyButtonContainer: {
paddingHorizontal: theme.spacing.lg,
paddingVertical: theme.spacing.lg,
backgroundColor: theme.colors.background,
borderTopWidth: 1,
borderTopColor: theme.colors.border,
},
// Continue button
continueButton: {
backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius.medium,
paddingVertical: theme.spacing.md,
paddingHorizontal: theme.spacing.lg,
alignItems: 'center',
...theme.shadows.primary,
},
// Continue button disabled
continueButtonDisabled: {
backgroundColor: theme.colors.border,
opacity: 0.6,
},
// Continue button text
continueButtonText: {
fontSize: theme.typography.fontSize.bodyLarge,
fontFamily: theme.typography.fontFamily.bold,
color: theme.colors.background,
},
// Continue button text disabled
continueButtonTextDisabled: {
color: theme.colors.textMuted,
},
});
export default HospitalSelectionStep;
/*
* End of File: HospitalSelectionStep.tsx
* Design & Developed by Tech4Biz Solutions
* Copyright (c) Spurrin Innovations. All rights reserved.
*/