WellNuo/app/(tabs)/index.tsx
Sergei ccf1701a34 Full sync with auth screens and discussion docs
Added:
- forgot-password.tsx, register.tsx auth screens
- Discussion_Points.md and Discussion_Points.txt

Updated:
- login, chat, index, beneficiary detail screens
- profile/help and profile/support
- API service
- Scheme files (Discussion, AppStore)

All assets/images are tracked and safe.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 17:04:46 -08:00

334 lines
9.0 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';
// Patient card component
interface PatientCardProps {
patient: Beneficiary;
onPress: () => void;
}
function PatientCard({ patient, onPress }: PatientCardProps) {
const wellnessColor = patient.wellness_score && patient.wellness_score >= 70
? AppColors.success
: patient.wellness_score && patient.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}>
{patient.avatar ? (
<Image source={{ uri: patient.avatar }} style={styles.avatarImage} />
) : (
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{patient.name.charAt(0).toUpperCase()}
</Text>
</View>
)}
</View>
{/* Info */}
<View style={styles.info}>
<Text style={styles.name}>{patient.name}</Text>
{patient.last_location && (
<View style={styles.locationRow}>
<Ionicons name="location-outline" size={12} color={AppColors.textSecondary} />
<Text style={styles.locationText}>{patient.last_location}</Text>
</View>
)}
{patient.last_activity && (
<Text style={styles.lastActivity}>{patient.last_activity}</Text>
)}
</View>
{/* Wellness Score */}
{patient.wellness_score !== undefined && (
<View style={styles.wellnessContainer}>
<Text style={[styles.wellnessScore, { color: wellnessColor }]}>
{patient.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 [patients, setPatients] = useState<Beneficiary[]>([]);
const [error, setError] = useState<string | null>(null);
// Load patients from API
useEffect(() => {
loadPatients();
}, []);
const loadPatients = async () => {
setIsLoading(true);
setError(null);
try {
const response = await api.getAllPatients();
if (response.ok && response.data) {
setPatients(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 patients');
}
} catch (err) {
console.error('Failed to load patients:', err);
setError('Failed to load patients');
} finally {
setIsLoading(false);
}
};
const handleRefresh = async () => {
setIsRefreshing(true);
await loadPatients();
setIsRefreshing(false);
};
const handlePatientPress = (patient: Beneficiary) => {
// Set current beneficiary in context
setCurrentBeneficiary(patient);
// Navigate to patient dashboard with deployment_id
router.push(`/(tabs)/beneficiaries/${patient.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 patients...</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>
{/* Patient List */}
{patients.length === 0 ? (
<View style={styles.emptyContainer}>
<Ionicons name="people-outline" size={64} color={AppColors.textMuted} />
<Text style={styles.emptyTitle}>No Patients</Text>
<Text style={styles.emptyText}>You don't have any patients assigned yet.</Text>
</View>
) : (
<FlatList
data={patients}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<PatientCard
patient={item}
onPress={() => handlePatientPress(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,
},
});