Sergei 24babce3c8 v1.0.0 - First stable release
Stable version with:
- WellNuo mobile app (React Native + Expo)
- Beneficiaries management
- Dashboard integration
- API documentation in wellnuoSheme/
- App icons and assets
- EAS build configuration

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 14:03:03 -08:00

387 lines
12 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
RefreshControl,
} from 'react-native';
import { useLocalSearchParams, router } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { api } from '@/services/api';
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { FullScreenError } from '@/components/ui/ErrorMessage';
import { Button } from '@/components/ui/Button';
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
import type { Beneficiary } from '@/types';
export default function BeneficiaryDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { setCurrentBeneficiary } = useBeneficiary();
const [beneficiary, setBeneficiary] = useState<Beneficiary | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadBeneficiary = useCallback(async (showLoading = true) => {
if (!id) return;
if (showLoading) setIsLoading(true);
setError(null);
try {
const response = await api.getBeneficiary(parseInt(id, 10));
if (response.ok && response.data) {
setBeneficiary(response.data);
} else {
setError(response.error?.message || 'Failed to load beneficiary');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, [id]);
useEffect(() => {
loadBeneficiary();
}, [loadBeneficiary]);
const handleRefresh = useCallback(() => {
setIsRefreshing(true);
loadBeneficiary(false);
}, [loadBeneficiary]);
const handleChatPress = () => {
// Set current beneficiary in context before navigating to chat
// This allows the chat to include beneficiary context in AI questions
if (beneficiary) {
setCurrentBeneficiary(beneficiary);
}
router.push('/(tabs)/chat');
};
if (isLoading) {
return <LoadingSpinner fullScreen message="Loading beneficiary data..." />;
}
if (error || !beneficiary) {
return (
<FullScreenError
message={error || 'Beneficiary not found'}
onRetry={() => loadBeneficiary()}
/>
);
}
return (
<SafeAreaView style={styles.container} edges={['top']}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
<Text style={styles.headerTitle}>{beneficiary.name}</Text>
<TouchableOpacity style={styles.menuButton}>
<Ionicons name="ellipsis-vertical" size={24} color={AppColors.textPrimary} />
</TouchableOpacity>
</View>
<ScrollView
style={styles.content}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={handleRefresh}
tintColor={AppColors.primary}
/>
}
>
{/* Beneficiary Info Card */}
<View style={styles.infoCard}>
<View style={styles.avatarContainer}>
<Text style={styles.avatarText}>
{beneficiary.name.charAt(0).toUpperCase()}
</Text>
<View
style={[
styles.statusBadge,
beneficiary.status === 'online' ? styles.onlineBadge : styles.offlineBadge,
]}
>
<Text style={styles.statusText}>
{beneficiary.status === 'online' ? 'Online' : 'Offline'}
</Text>
</View>
</View>
<Text style={styles.beneficiaryName}>{beneficiary.name}</Text>
<Text style={styles.relationship}>{beneficiary.relationship}</Text>
<Text style={styles.lastSeen}>
Last activity: {beneficiary.last_activity}
</Text>
</View>
{/* Sensor Stats */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Sensor Overview</Text>
<View style={styles.statsGrid}>
<View style={styles.statCard}>
<View style={[styles.statIcon, { backgroundColor: beneficiary.sensor_data?.motion_detected ? '#D1FAE5' : '#F3F4F6' }]}>
<Ionicons
name={beneficiary.sensor_data?.motion_detected ? "walk" : "walk-outline"}
size={24}
color={beneficiary.sensor_data?.motion_detected ? AppColors.success : AppColors.textMuted}
/>
</View>
<Text style={styles.statValue}>
{beneficiary.sensor_data?.motion_detected ? 'Active' : 'Inactive'}
</Text>
<Text style={styles.statLabel}>Motion</Text>
<Text style={styles.statUnit}>{beneficiary.sensor_data?.last_motion || '--'}</Text>
</View>
<View style={styles.statCard}>
<View style={[styles.statIcon, { backgroundColor: beneficiary.sensor_data?.door_status === 'open' ? '#FEF3C7' : '#DBEAFE' }]}>
<Ionicons
name={beneficiary.sensor_data?.door_status === 'open' ? "enter-outline" : "home-outline"}
size={24}
color={beneficiary.sensor_data?.door_status === 'open' ? AppColors.warning : AppColors.primary}
/>
</View>
<Text style={styles.statValue}>
{beneficiary.sensor_data?.door_status === 'open' ? 'Open' : 'Closed'}
</Text>
<Text style={styles.statLabel}>Door Status</Text>
<Text style={styles.statUnit}>Main entrance</Text>
</View>
<View style={styles.statCard}>
<View style={[styles.statIcon, { backgroundColor: '#E0E7FF' }]}>
<Ionicons name="thermometer-outline" size={24} color={AppColors.primaryDark} />
</View>
<Text style={styles.statValue}>
{beneficiary.sensor_data?.temperature || '--'}°C
</Text>
<Text style={styles.statLabel}>Temperature</Text>
<Text style={styles.statUnit}>{beneficiary.sensor_data?.humidity || '--'}% humidity</Text>
</View>
</View>
</View>
{/* Quick Actions */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Quick Actions</Text>
<View style={styles.actionsGrid}>
<TouchableOpacity style={styles.actionCard} onPress={handleChatPress}>
<View style={[styles.actionIcon, { backgroundColor: '#D1FAE5' }]}>
<Ionicons name="chatbubble-ellipses" size={24} color={AppColors.success} />
</View>
<Text style={styles.actionLabel}>Chat with Julia</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionCard}>
<View style={[styles.actionIcon, { backgroundColor: '#FEF3C7' }]}>
<Ionicons name="notifications" size={24} color={AppColors.warning} />
</View>
<Text style={styles.actionLabel}>Set Reminder</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionCard}>
<View style={[styles.actionIcon, { backgroundColor: '#DBEAFE' }]}>
<Ionicons name="call" size={24} color={AppColors.primary} />
</View>
<Text style={styles.actionLabel}>Video Call</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionCard}>
<View style={[styles.actionIcon, { backgroundColor: '#F3E8FF' }]}>
<Ionicons name="analytics" size={24} color="#9333EA" />
</View>
<Text style={styles.actionLabel}>Activity Report</Text>
</TouchableOpacity>
</View>
</View>
{/* Chat with Julia Button */}
<View style={styles.chatButtonContainer}>
<Button
title="Chat with Julia AI"
onPress={handleChatPress}
fullWidth
size="lg"
/>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.surface,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
backgroundColor: AppColors.background,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
backButton: {
padding: Spacing.xs,
},
headerTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
},
menuButton: {
padding: Spacing.xs,
},
content: {
flex: 1,
},
infoCard: {
backgroundColor: AppColors.background,
padding: Spacing.xl,
alignItems: 'center',
marginBottom: Spacing.md,
},
avatarContainer: {
width: 100,
height: 100,
borderRadius: BorderRadius.full,
backgroundColor: AppColors.primaryLight,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.md,
},
avatarText: {
fontSize: FontSizes['3xl'],
fontWeight: '600',
color: AppColors.white,
},
statusBadge: {
position: 'absolute',
bottom: 0,
paddingHorizontal: Spacing.sm,
paddingVertical: Spacing.xs,
borderRadius: BorderRadius.full,
},
onlineBadge: {
backgroundColor: AppColors.success,
},
offlineBadge: {
backgroundColor: AppColors.offline,
},
statusText: {
fontSize: FontSizes.xs,
fontWeight: '500',
color: AppColors.white,
},
beneficiaryName: {
fontSize: FontSizes['2xl'],
fontWeight: '700',
color: AppColors.textPrimary,
},
relationship: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
marginTop: Spacing.xs,
},
lastSeen: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
marginTop: Spacing.sm,
},
section: {
backgroundColor: AppColors.background,
padding: Spacing.lg,
marginBottom: Spacing.md,
},
sectionTitle: {
fontSize: FontSizes.lg,
fontWeight: '600',
color: AppColors.textPrimary,
marginBottom: Spacing.md,
},
statsGrid: {
flexDirection: 'row',
justifyContent: 'space-between',
},
statCard: {
flex: 1,
alignItems: 'center',
padding: Spacing.sm,
},
statIcon: {
width: 48,
height: 48,
borderRadius: BorderRadius.lg,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.sm,
},
statValue: {
fontSize: FontSizes.xl,
fontWeight: '700',
color: AppColors.textPrimary,
},
statLabel: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
marginTop: Spacing.xs,
},
statUnit: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
actionsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
},
actionCard: {
width: '48%',
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
alignItems: 'center',
marginBottom: Spacing.md,
},
actionIcon: {
width: 48,
height: 48,
borderRadius: BorderRadius.lg,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.sm,
},
actionLabel: {
fontSize: FontSizes.sm,
fontWeight: '500',
color: AppColors.textPrimary,
textAlign: 'center',
},
chatButtonContainer: {
padding: Spacing.lg,
paddingBottom: Spacing.xxl,
},
});