WellNuo/app/(tabs)/index.tsx
Sergei 19d24e7b00 Rename patient to beneficiary throughout the app
- Replace all 'patient' terminology with 'beneficiary'
- Add Voice AI screen (voice.tsx) with voice_ask API integration
- Optimize getAllBeneficiaries() to use single deployments_list API call
- Rename PatientDashboardData to BeneficiaryDashboardData
- Update UI components: BeneficiaryCard, beneficiary picker modal
- Update all error messages and comments

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 12:57:48 -08:00

334 lines
9.2 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
ActivityIndicator,
RefreshControl,
Image,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router } from 'expo-router';
import { useAuth } from '@/contexts/AuthContext';
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { api } from '@/services/api';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import type { Beneficiary } from '@/types';
// Beneficiary card component
interface BeneficiaryCardProps {
beneficiary: Beneficiary;
onPress: () => void;
}
function BeneficiaryCard({ beneficiary, onPress }: BeneficiaryCardProps) {
const wellnessColor = beneficiary.wellness_score && beneficiary.wellness_score >= 70
? AppColors.success
: beneficiary.wellness_score && beneficiary.wellness_score >= 40
? '#F59E0B'
: AppColors.error;
return (
<TouchableOpacity style={styles.card} onPress={onPress} activeOpacity={0.7}>
<View style={styles.cardContent}>
{/* Avatar */}
<View style={styles.avatarWrapper}>
{beneficiary.avatar ? (
<Image source={{ uri: beneficiary.avatar }} style={styles.avatarImage} />
) : (
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{beneficiary.name.charAt(0).toUpperCase()}
</Text>
</View>
)}
</View>
{/* Info */}
<View style={styles.info}>
<Text style={styles.name}>{beneficiary.name}</Text>
{beneficiary.last_location && (
<View style={styles.locationRow}>
<Ionicons name="location-outline" size={12} color={AppColors.textSecondary} />
<Text style={styles.locationText}>{beneficiary.last_location}</Text>
</View>
)}
{beneficiary.last_activity && (
<Text style={styles.lastActivity}>{beneficiary.last_activity}</Text>
)}
</View>
{/* Wellness Score */}
{beneficiary.wellness_score !== undefined && (
<View style={styles.wellnessContainer}>
<Text style={[styles.wellnessScore, { color: wellnessColor }]}>
{beneficiary.wellness_score}%
</Text>
<Text style={styles.wellnessLabel}>Wellness</Text>
</View>
)}
{/* Arrow */}
<Ionicons name="chevron-forward" size={24} color={AppColors.textMuted} />
</View>
</TouchableOpacity>
);
}
export default function HomeScreen() {
const { user } = useAuth();
const { currentBeneficiary, setCurrentBeneficiary } = useBeneficiary();
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [beneficiaries, setBeneficiaries] = useState<Beneficiary[]>([]);
const [error, setError] = useState<string | null>(null);
// Load beneficiaries from API
useEffect(() => {
loadBeneficiaries();
}, []);
const loadBeneficiaries = async () => {
setIsLoading(true);
setError(null);
try {
const response = await api.getAllBeneficiaries();
if (response.ok && response.data) {
setBeneficiaries(response.data);
// Auto-select first beneficiary if none selected
if (!currentBeneficiary && response.data.length > 0) {
setCurrentBeneficiary(response.data[0]);
}
} else {
setError(response.error?.message || 'Failed to load beneficiaries');
}
} catch (err) {
console.error('Failed to load beneficiaries:', err);
setError('Failed to load beneficiaries');
} finally {
setIsLoading(false);
}
};
const handleRefresh = async () => {
setIsRefreshing(true);
await loadBeneficiaries();
setIsRefreshing(false);
};
const handleBeneficiaryPress = (beneficiary: Beneficiary) => {
// Set current beneficiary in context
setCurrentBeneficiary(beneficiary);
// Navigate to beneficiary dashboard with deployment_id
router.push(`/(tabs)/beneficiaries/${beneficiary.id}/dashboard`);
};
if (isLoading) {
return (
<SafeAreaView style={styles.container} edges={['top']}>
<View style={styles.header}>
<View>
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
<Text style={styles.headerTitle}>My Beneficiaries</Text>
</View>
</View>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.loadingText}>Loading beneficiaries...</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top']}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
<Text style={styles.headerTitle}>My Beneficiaries</Text>
</View>
<TouchableOpacity style={styles.refreshButton} onPress={handleRefresh}>
<Ionicons name="refresh" size={22} color={AppColors.primary} />
</TouchableOpacity>
</View>
{/* Beneficiary List */}
{beneficiaries.length === 0 ? (
<View style={styles.emptyContainer}>
<Ionicons name="people-outline" size={64} color={AppColors.textMuted} />
<Text style={styles.emptyTitle}>No Beneficiaries</Text>
<Text style={styles.emptyText}>You don't have any beneficiaries assigned yet.</Text>
</View>
) : (
<FlatList
data={beneficiaries}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<BeneficiaryCard
beneficiary={item}
onPress={() => handleBeneficiaryPress(item)}
/>
)}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
colors={[AppColors.primary]}
tintColor={AppColors.primary}
/>
}
/>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: Spacing.lg,
paddingVertical: Spacing.md,
backgroundColor: AppColors.background,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
greeting: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
},
headerTitle: {
fontSize: FontSizes.xl,
fontWeight: '700',
color: AppColors.textPrimary,
},
refreshButton: {
padding: Spacing.xs,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: Spacing.md,
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: Spacing.xl,
},
emptyTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
marginTop: Spacing.md,
},
emptyText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
textAlign: 'center',
marginTop: Spacing.xs,
},
listContent: {
padding: Spacing.lg,
paddingBottom: Spacing.xxl,
},
// Card styles
card: {
backgroundColor: AppColors.white,
borderRadius: BorderRadius.lg,
marginBottom: Spacing.md,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
cardContent: {
flexDirection: 'row',
alignItems: 'center',
padding: Spacing.md,
},
avatarWrapper: {
width: 56,
height: 56,
borderRadius: 28,
position: 'relative',
overflow: 'hidden',
},
avatar: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: AppColors.primaryLight,
justifyContent: 'center',
alignItems: 'center',
},
avatarImage: {
width: 56,
height: 56,
borderRadius: 28,
},
avatarText: {
fontSize: FontSizes.xl,
fontWeight: '600',
color: AppColors.white,
},
info: {
flex: 1,
marginLeft: Spacing.md,
},
name: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
},
relationship: {
fontSize: FontSizes.sm,
color: AppColors.textSecondary,
marginTop: 2,
},
lastActivity: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginLeft: Spacing.sm,
},
locationRow: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
},
locationText: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
marginLeft: 4,
},
wellnessContainer: {
alignItems: 'center',
marginRight: Spacing.sm,
},
wellnessScore: {
fontSize: FontSizes.lg,
fontWeight: '700',
},
wellnessLabel: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
});