WellNuo/app/(tabs)/index.tsx
Sergei af148faa40 Add scheme files, beneficiaries module, dashboard improvements
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>
2025-12-12 13:38:38 -08:00

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,
},
});