610 lines
17 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
RefreshControl,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native-safe-area-context';
import { router, useLocalSearchParams } from 'expo-router';
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
import { api } from '@/services/api';
import {
AppColors,
BorderRadius,
FontSizes,
FontWeights,
Spacing,
Shadows,
} from '@/constants/theme';
interface Device {
id: string;
name: string;
type: 'motion' | 'door' | 'temperature' | 'hub';
status: 'online' | 'offline';
lastSeen?: string;
room?: string;
}
const deviceTypeConfig = {
motion: {
icon: 'body-outline' as const,
label: 'Motion Sensor',
color: AppColors.primary,
bgColor: AppColors.primaryLighter,
},
door: {
icon: 'enter-outline' as const,
label: 'Door Sensor',
color: AppColors.info,
bgColor: AppColors.infoLight,
},
temperature: {
icon: 'thermometer-outline' as const,
label: 'Temperature',
color: AppColors.warning,
bgColor: AppColors.warningLight,
},
hub: {
icon: 'git-network-outline' as const,
label: 'Hub',
color: AppColors.accent,
bgColor: AppColors.accentLight,
},
};
export default function EquipmentScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const { currentBeneficiary } = useBeneficiary();
const [devices, setDevices] = useState<Device[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isDetaching, setIsDetaching] = useState<string | null>(null);
const beneficiaryName = currentBeneficiary?.name || 'this person';
useEffect(() => {
loadDevices();
}, [id]);
const loadDevices = async () => {
if (!id) return;
try {
// For now, mock data - replace with actual API call
// const response = await api.getDevices(id);
// Mock devices for demonstration
const mockDevices: Device[] = [
{
id: '1',
name: 'Living Room Motion',
type: 'motion',
status: 'online',
lastSeen: '2 min ago',
room: 'Living Room',
},
{
id: '2',
name: 'Front Door',
type: 'door',
status: 'online',
lastSeen: '5 min ago',
room: 'Entrance',
},
{
id: '3',
name: 'Bedroom Motion',
type: 'motion',
status: 'offline',
lastSeen: '2 hours ago',
room: 'Bedroom',
},
{
id: '4',
name: 'Temperature Monitor',
type: 'temperature',
status: 'online',
lastSeen: '1 min ago',
room: 'Kitchen',
},
{
id: '5',
name: 'WellNuo Hub',
type: 'hub',
status: 'online',
lastSeen: 'Just now',
},
];
setDevices(mockDevices);
} catch (error) {
console.error('Failed to load devices:', error);
Alert.alert('Error', 'Failed to load devices');
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
};
const handleRefresh = useCallback(() => {
setIsRefreshing(true);
loadDevices();
}, [id]);
const handleDetachDevice = (device: Device) => {
Alert.alert(
'Detach Device',
`Are you sure you want to detach "${device.name}" from ${beneficiaryName}?\n\nThe device will become available for use with another person.`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Detach',
style: 'destructive',
onPress: async () => {
setIsDetaching(device.id);
try {
// API call to detach device
// await api.detachDevice(id, device.id);
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Remove from local state
setDevices(prev => prev.filter(d => d.id !== device.id));
Alert.alert('Success', `${device.name} has been detached.`);
} catch (error) {
Alert.alert('Error', 'Failed to detach device. Please try again.');
} finally {
setIsDetaching(null);
}
},
},
]
);
};
const handleDetachAll = () => {
if (devices.length === 0) {
Alert.alert('No Devices', 'There are no devices to detach.');
return;
}
Alert.alert(
'Detach All Devices',
`Are you sure you want to detach all ${devices.length} devices from ${beneficiaryName}?\n\nThis action cannot be undone.`,
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Detach All',
style: 'destructive',
onPress: async () => {
setIsLoading(true);
try {
// API call to detach all devices
// await api.detachAllDevices(id);
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1500));
setDevices([]);
Alert.alert('Success', 'All devices have been detached.');
} catch (error) {
Alert.alert('Error', 'Failed to detach devices. Please try again.');
} finally {
setIsLoading(false);
}
},
},
]
);
};
const handleAddDevice = () => {
router.push({
pathname: '/(auth)/activate',
params: { lovedOneName: beneficiaryName, beneficiaryId: id },
});
};
if (isLoading) {
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
<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}>Equipment</Text>
<View style={styles.placeholder} />
</View>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.loadingText}>Loading devices...</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
{/* 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}>Equipment</Text>
<TouchableOpacity style={styles.addButton} onPress={handleAddDevice}>
<Ionicons name="add" size={24} color={AppColors.primary} />
</TouchableOpacity>
</View>
<ScrollView
style={styles.content}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />
}
>
{/* Summary Card */}
<View style={styles.summaryCard}>
<View style={styles.summaryRow}>
<View style={styles.summaryItem}>
<Text style={styles.summaryValue}>{devices.length}</Text>
<Text style={styles.summaryLabel}>Total Devices</Text>
</View>
<View style={styles.summaryDivider} />
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: AppColors.success }]}>
{devices.filter(d => d.status === 'online').length}
</Text>
<Text style={styles.summaryLabel}>Online</Text>
</View>
<View style={styles.summaryDivider} />
<View style={styles.summaryItem}>
<Text style={[styles.summaryValue, { color: AppColors.error }]}>
{devices.filter(d => d.status === 'offline').length}
</Text>
<Text style={styles.summaryLabel}>Offline</Text>
</View>
</View>
</View>
{/* Devices List */}
{devices.length === 0 ? (
<View style={styles.emptyState}>
<View style={styles.emptyIconContainer}>
<Ionicons name="hardware-chip-outline" size={48} color={AppColors.textMuted} />
</View>
<Text style={styles.emptyTitle}>No Devices Connected</Text>
<Text style={styles.emptyText}>
Add sensors to start monitoring {beneficiaryName}'s wellness.
</Text>
<TouchableOpacity style={styles.addDeviceButton} onPress={handleAddDevice}>
<Ionicons name="add" size={20} color={AppColors.white} />
<Text style={styles.addDeviceButtonText}>Add Device</Text>
</TouchableOpacity>
</View>
) : (
<>
<Text style={styles.sectionTitle}>Connected Devices</Text>
<View style={styles.devicesList}>
{devices.map((device) => {
const config = deviceTypeConfig[device.type];
const isDetachingThis = isDetaching === device.id;
return (
<View key={device.id} style={styles.deviceCard}>
<View style={styles.deviceInfo}>
<View style={[styles.deviceIcon, { backgroundColor: config.bgColor }]}>
<Ionicons name={config.icon} size={22} color={config.color} />
</View>
<View style={styles.deviceDetails}>
<Text style={styles.deviceName}>{device.name}</Text>
<View style={styles.deviceMeta}>
<View style={[
styles.statusDot,
{ backgroundColor: device.status === 'online' ? AppColors.success : AppColors.error }
]} />
<Text style={styles.deviceStatus}>
{device.status === 'online' ? 'Online' : 'Offline'}
</Text>
{device.room && (
<>
<Text style={styles.deviceMetaSeparator}>•</Text>
<Text style={styles.deviceRoom}>{device.room}</Text>
</>
)}
</View>
</View>
</View>
<TouchableOpacity
style={styles.detachButton}
onPress={() => handleDetachDevice(device)}
disabled={isDetachingThis}
>
{isDetachingThis ? (
<ActivityIndicator size="small" color={AppColors.error} />
) : (
<Ionicons name="unlink-outline" size={20} color={AppColors.error} />
)}
</TouchableOpacity>
</View>
);
})}
</View>
{/* Detach All Button */}
{devices.length > 1 && (
<TouchableOpacity style={styles.detachAllButton} onPress={handleDetachAll}>
<Ionicons name="trash-outline" size={20} color={AppColors.error} />
<Text style={styles.detachAllText}>Detach All Devices</Text>
</TouchableOpacity>
)}
</>
)}
{/* Info Section */}
<View style={styles.infoCard}>
<View style={styles.infoHeader}>
<Ionicons name="information-circle" size={20} color={AppColors.info} />
<Text style={styles.infoTitle}>About Equipment</Text>
</View>
<Text style={styles.infoText}>
Detaching a device will remove it from {beneficiaryName}'s monitoring setup.
You can then attach it to another person or re-attach it later using the activation code.
</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: AppColors.background,
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: Spacing.md,
paddingVertical: Spacing.sm,
borderBottomWidth: 1,
borderBottomColor: AppColors.border,
},
backButton: {
padding: Spacing.xs,
},
headerTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
placeholder: {
width: 32,
},
addButton: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
backgroundColor: AppColors.primaryLighter,
justifyContent: 'center',
alignItems: 'center',
},
content: {
flex: 1,
},
scrollContent: {
padding: Spacing.lg,
paddingBottom: Spacing.xxl,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
gap: Spacing.md,
},
loadingText: {
fontSize: FontSizes.base,
color: AppColors.textSecondary,
},
// Summary Card
summaryCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
padding: Spacing.lg,
marginBottom: Spacing.lg,
...Shadows.sm,
},
summaryRow: {
flexDirection: 'row',
alignItems: 'center',
},
summaryItem: {
flex: 1,
alignItems: 'center',
},
summaryValue: {
fontSize: FontSizes['2xl'],
fontWeight: FontWeights.bold,
color: AppColors.textPrimary,
},
summaryLabel: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
marginTop: 2,
},
summaryDivider: {
width: 1,
height: 32,
backgroundColor: AppColors.border,
},
// Section Title
sectionTitle: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.semibold,
color: AppColors.textSecondary,
marginBottom: Spacing.md,
textTransform: 'uppercase',
letterSpacing: 0.5,
},
// Devices List
devicesList: {
gap: Spacing.md,
},
deviceCard: {
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
...Shadows.xs,
},
deviceInfo: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.md,
},
deviceIcon: {
width: 48,
height: 48,
borderRadius: BorderRadius.lg,
justifyContent: 'center',
alignItems: 'center',
},
deviceDetails: {
flex: 1,
},
deviceName: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
},
deviceMeta: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 2,
gap: 4,
},
statusDot: {
width: 6,
height: 6,
borderRadius: 3,
},
deviceStatus: {
fontSize: FontSizes.xs,
color: AppColors.textSecondary,
},
deviceMetaSeparator: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
deviceRoom: {
fontSize: FontSizes.xs,
color: AppColors.textMuted,
},
detachButton: {
width: 40,
height: 40,
borderRadius: BorderRadius.md,
backgroundColor: AppColors.errorLight,
justifyContent: 'center',
alignItems: 'center',
},
// Empty State
emptyState: {
alignItems: 'center',
padding: Spacing.xl,
backgroundColor: AppColors.surface,
borderRadius: BorderRadius.xl,
...Shadows.sm,
},
emptyIconContainer: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: AppColors.surfaceSecondary,
justifyContent: 'center',
alignItems: 'center',
marginBottom: Spacing.md,
},
emptyTitle: {
fontSize: FontSizes.lg,
fontWeight: FontWeights.semibold,
color: AppColors.textPrimary,
marginBottom: Spacing.xs,
},
emptyText: {
fontSize: FontSizes.sm,
color: AppColors.textMuted,
textAlign: 'center',
marginBottom: Spacing.lg,
},
addDeviceButton: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: AppColors.primary,
paddingVertical: Spacing.sm,
paddingHorizontal: Spacing.lg,
borderRadius: BorderRadius.lg,
gap: Spacing.xs,
},
addDeviceButtonText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.white,
},
// Detach All Button
detachAllButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: AppColors.errorLight,
paddingVertical: Spacing.md,
borderRadius: BorderRadius.lg,
marginTop: Spacing.lg,
gap: Spacing.sm,
},
detachAllText: {
fontSize: FontSizes.base,
fontWeight: FontWeights.semibold,
color: AppColors.error,
},
// Info Card
infoCard: {
backgroundColor: AppColors.infoLight,
borderRadius: BorderRadius.lg,
padding: Spacing.md,
marginTop: Spacing.xl,
},
infoHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: Spacing.sm,
marginBottom: Spacing.xs,
},
infoTitle: {
fontSize: FontSizes.sm,
fontWeight: FontWeights.semibold,
color: AppColors.info,
},
infoText: {
fontSize: FontSizes.sm,
color: AppColors.info,
lineHeight: 20,
},
});