Changes: - Add wellnuoSheme/ folder with project documentation - Rename patients -> beneficiaries (proper WellNuo terminology) - Add BeneficiaryContext for state management - Update API service with WellNuo endpoints - Add dashboard screen for beneficiary overview - Update navigation and layout Scheme files include: - API documentation with credentials - Project description - System analysis - UX flow - Legal documents (privacy, terms, support) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
329 lines
9.3 KiB
TypeScript
329 lines
9.3 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
FlatList,
|
|
TouchableOpacity,
|
|
RefreshControl,
|
|
} from 'react-native';
|
|
import { router } from 'expo-router';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { api } from '@/services/api';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
|
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
|
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
|
import type { Beneficiary } from '@/types';
|
|
|
|
export default function BeneficiariesListScreen() {
|
|
const { user } = useAuth();
|
|
const { setCurrentBeneficiary } = useBeneficiary();
|
|
const [beneficiaries, setBeneficiaries] = useState<Beneficiary[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const loadBeneficiaries = useCallback(async (showLoading = true) => {
|
|
if (showLoading) setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await api.getBeneficiaries();
|
|
|
|
if (response.ok && response.data) {
|
|
setBeneficiaries(response.data.beneficiaries);
|
|
} else {
|
|
setError(response.error?.message || 'Failed to load beneficiaries');
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'An error occurred');
|
|
} finally {
|
|
setIsLoading(false);
|
|
setIsRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadBeneficiaries();
|
|
}, [loadBeneficiaries]);
|
|
|
|
const handleRefresh = useCallback(() => {
|
|
setIsRefreshing(true);
|
|
loadBeneficiaries(false);
|
|
}, [loadBeneficiaries]);
|
|
|
|
const handleBeneficiaryPress = (beneficiary: Beneficiary) => {
|
|
// Set current beneficiary in context before navigating
|
|
setCurrentBeneficiary(beneficiary);
|
|
// Navigate directly to their dashboard
|
|
router.push(`/beneficiaries/${beneficiary.id}/dashboard`);
|
|
};
|
|
|
|
const renderBeneficiaryCard = ({ item }: { item: Beneficiary }) => (
|
|
<TouchableOpacity
|
|
style={styles.beneficiaryCard}
|
|
onPress={() => handleBeneficiaryPress(item)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View style={styles.beneficiaryInfo}>
|
|
<View style={styles.avatarContainer}>
|
|
<Text style={styles.avatarText}>
|
|
{item.name.charAt(0).toUpperCase()}
|
|
</Text>
|
|
<View
|
|
style={[
|
|
styles.statusIndicator,
|
|
item.status === 'online' ? styles.online : styles.offline,
|
|
]}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.beneficiaryDetails}>
|
|
<Text style={styles.beneficiaryName}>{item.name}</Text>
|
|
<Text style={styles.beneficiaryRelationship}>{item.relationship}</Text>
|
|
<Text style={styles.lastActivity}>
|
|
<Ionicons name="time-outline" size={12} color={AppColors.textMuted} />{' '}
|
|
{item.last_activity}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{item.sensor_data && (
|
|
<View style={styles.sensorStats}>
|
|
<View style={styles.statItem}>
|
|
<Ionicons
|
|
name={item.sensor_data.motion_detected ? "walk" : "walk-outline"}
|
|
size={16}
|
|
color={item.sensor_data.motion_detected ? AppColors.online : AppColors.textMuted}
|
|
/>
|
|
<Text style={styles.statValue}>
|
|
{item.sensor_data.motion_detected ? 'Active' : 'Inactive'}
|
|
</Text>
|
|
<Text style={styles.statLabel}>Motion</Text>
|
|
</View>
|
|
<View style={styles.statItem}>
|
|
<Ionicons
|
|
name={item.sensor_data.door_status === 'open' ? "enter-outline" : "home-outline"}
|
|
size={16}
|
|
color={item.sensor_data.door_status === 'open' ? AppColors.warning : AppColors.primary}
|
|
/>
|
|
<Text style={styles.statValue}>
|
|
{item.sensor_data.door_status === 'open' ? 'Open' : 'Closed'}
|
|
</Text>
|
|
<Text style={styles.statLabel}>Door</Text>
|
|
</View>
|
|
<View style={styles.statItem}>
|
|
<Ionicons name="thermometer-outline" size={16} color={AppColors.primaryDark} />
|
|
<Text style={styles.statValue}>{item.sensor_data.temperature}°</Text>
|
|
<Text style={styles.statLabel}>Temp</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
|
|
<Ionicons
|
|
name="chevron-forward"
|
|
size={20}
|
|
color={AppColors.textMuted}
|
|
style={styles.chevron}
|
|
/>
|
|
</TouchableOpacity>
|
|
);
|
|
|
|
if (isLoading) {
|
|
return <LoadingSpinner fullScreen message="Loading beneficiaries..." />;
|
|
}
|
|
|
|
if (error) {
|
|
return <FullScreenError message={error} onRetry={() => loadBeneficiaries()} />;
|
|
}
|
|
|
|
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}>Beneficiaries</Text>
|
|
</View>
|
|
<TouchableOpacity style={styles.addButton}>
|
|
<Ionicons name="add" size={24} color={AppColors.white} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Beneficiary List */}
|
|
<FlatList
|
|
data={beneficiaries}
|
|
keyExtractor={(item) => item.id.toString()}
|
|
renderItem={renderBeneficiaryCard}
|
|
contentContainerStyle={styles.listContent}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={isRefreshing}
|
|
onRefresh={handleRefresh}
|
|
tintColor={AppColors.primary}
|
|
/>
|
|
}
|
|
ListEmptyComponent={
|
|
<View style={styles.emptyContainer}>
|
|
<Ionicons name="people-outline" size={64} color={AppColors.textMuted} />
|
|
<Text style={styles.emptyTitle}>No beneficiaries yet</Text>
|
|
<Text style={styles.emptyText}>
|
|
Add your first beneficiary to start monitoring
|
|
</Text>
|
|
</View>
|
|
}
|
|
/>
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: AppColors.surface,
|
|
},
|
|
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,
|
|
},
|
|
addButton: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: BorderRadius.full,
|
|
backgroundColor: AppColors.primary,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
listContent: {
|
|
padding: Spacing.md,
|
|
},
|
|
beneficiaryCard: {
|
|
backgroundColor: AppColors.background,
|
|
borderRadius: BorderRadius.lg,
|
|
padding: Spacing.md,
|
|
marginBottom: Spacing.md,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.05,
|
|
shadowRadius: 4,
|
|
elevation: 2,
|
|
},
|
|
beneficiaryInfo: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: Spacing.md,
|
|
},
|
|
avatarContainer: {
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: BorderRadius.full,
|
|
backgroundColor: AppColors.primaryLight,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginRight: Spacing.md,
|
|
},
|
|
avatarText: {
|
|
fontSize: FontSizes.xl,
|
|
fontWeight: '600',
|
|
color: AppColors.white,
|
|
},
|
|
statusIndicator: {
|
|
position: 'absolute',
|
|
bottom: 2,
|
|
right: 2,
|
|
width: 14,
|
|
height: 14,
|
|
borderRadius: BorderRadius.full,
|
|
borderWidth: 2,
|
|
borderColor: AppColors.background,
|
|
},
|
|
online: {
|
|
backgroundColor: AppColors.online,
|
|
},
|
|
offline: {
|
|
backgroundColor: AppColors.offline,
|
|
},
|
|
beneficiaryDetails: {
|
|
flex: 1,
|
|
},
|
|
beneficiaryName: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: '600',
|
|
color: AppColors.textPrimary,
|
|
},
|
|
beneficiaryRelationship: {
|
|
fontSize: FontSizes.sm,
|
|
color: AppColors.textSecondary,
|
|
marginTop: 2,
|
|
},
|
|
lastActivity: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
marginTop: 4,
|
|
},
|
|
sensorStats: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-around',
|
|
paddingTop: Spacing.md,
|
|
borderTopWidth: 1,
|
|
borderTopColor: AppColors.border,
|
|
},
|
|
statItem: {
|
|
alignItems: 'center',
|
|
},
|
|
statValue: {
|
|
fontSize: FontSizes.base,
|
|
fontWeight: '600',
|
|
color: AppColors.textPrimary,
|
|
marginTop: Spacing.xs,
|
|
},
|
|
statLabel: {
|
|
fontSize: FontSizes.xs,
|
|
color: AppColors.textMuted,
|
|
},
|
|
chevron: {
|
|
position: 'absolute',
|
|
top: Spacing.md,
|
|
right: Spacing.md,
|
|
},
|
|
emptyContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
paddingTop: Spacing.xxl * 2,
|
|
},
|
|
emptyTitle: {
|
|
fontSize: FontSizes.lg,
|
|
fontWeight: '600',
|
|
color: AppColors.textPrimary,
|
|
marginTop: Spacing.md,
|
|
},
|
|
emptyText: {
|
|
fontSize: FontSizes.base,
|
|
color: AppColors.textSecondary,
|
|
textAlign: 'center',
|
|
marginTop: Spacing.xs,
|
|
},
|
|
});
|