import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, ActivityIndicator, RefreshControl, Platform, ActionSheetIOS, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router, useLocalSearchParams } from 'expo-router'; import * as Device from 'expo-device'; import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { useBLE } from '@/contexts/BLEContext'; import { api } from '@/services/api'; import type { WPSensor } from '@/types'; import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, Shadows, } from '@/constants/theme'; import { BeneficiaryMenu } from '@/components/ui/BeneficiaryMenu'; const sensorConfig = { icon: 'water' as const, label: 'WP Sensor', color: AppColors.primary, bgColor: AppColors.primaryLighter, }; export default function EquipmentScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const { currentBeneficiary } = useBeneficiary(); const { isBLEAvailable, scanDevices, stopScan, foundDevices, isScanning: isBLEScanning } = useBLE(); // Separate state for API sensors (attached) and BLE sensors (nearby) const [apiSensors, setApiSensors] = useState([]); const [bleSensors, setBleSensors] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [isDetaching, setIsDetaching] = useState(null); const beneficiaryName = currentBeneficiary?.name || 'this person'; useEffect(() => { loadSensors(); }, [id]); const loadSensors = async () => { if (!id) return; try { setIsLoading(true); // Get WP sensors from API (attached to beneficiary) const response = await api.getDevicesForBeneficiary(id); if (!response.ok) { // If error is "Not authenticated with Legacy API" or network error, // just show empty state without Alert console.warn('[Equipment] Could not load sensors:', response.error); setApiSensors([]); return; } setApiSensors(response.data || []); } catch (error) { console.error('[Equipment] Failed to load sensors:', error); // Show empty state instead of Alert setApiSensors([]); } finally { setIsLoading(false); setIsRefreshing(false); } }; const handleRefresh = useCallback(() => { setIsRefreshing(true); loadSensors(); }, [id]); // BLE Scan for nearby sensors const handleScanNearby = async () => { if (isBLEScanning) { // Stop scan stopScan(); return; } setBleSensors([]); // Clear previous results try { await scanDevices(); // foundDevices will be updated by BLEContext } catch (error) { console.error('[Equipment] BLE scan failed:', error); Alert.alert('Scan Failed', 'Could not scan for nearby sensors. Make sure Bluetooth is enabled.'); } }; // Effect to convert BLE foundDevices to WPSensor format useEffect(() => { if (foundDevices.length === 0) return; // Convert BLE devices to WPSensor format const nearbyWPSensors: WPSensor[] = foundDevices .filter((d: { name?: string }) => d.name?.startsWith('WP_')) // Only WP sensors .map((d: { id: string; name?: string }) => { // Parse WP__ format const parts = d.name!.split('_'); const wellId = parseInt(parts[1], 10) || 0; const mac = parts[2] || d.id.slice(-6); return { deviceId: d.id, wellId: wellId, mac: mac, name: d.name!, status: 'offline' as const, // Nearby but not attached lastSeen: new Date(), beneficiaryId: id!, deploymentId: 0, // Not attached yet source: 'ble' as const, // From BLE scan }; }); // Filter out sensors that are already in API list const apiDeviceIds = new Set(apiSensors.map(s => s.mac)); const uniqueBleSensors = nearbyWPSensors.filter(s => !apiDeviceIds.has(s.mac)); setBleSensors(uniqueBleSensors); }, [foundDevices, apiSensors, id]); // Handle sensor click - show action sheet for offline, navigate to settings for online const handleSensorPress = (sensor: WPSensor) => { // For offline API sensors - show reconnect options if (sensor.source === 'api' && sensor.status === 'offline') { if (Platform.OS === 'ios') { ActionSheetIOS.showActionSheetWithOptions( { title: `${sensor.name} is Offline`, message: `Last seen: ${formatLastSeen(sensor.lastSeen)}`, options: ['Cancel', 'Reconnect via Bluetooth', 'Remove from this home'], destructiveButtonIndex: 2, cancelButtonIndex: 0, }, buttonIndex => { if (buttonIndex === 1) { // Reconnect - go to setup-wifi flow router.push(`/(tabs)/beneficiaries/${id}/setup-wifi?deviceId=${sensor.deviceId}&deviceName=${sensor.name}&wellId=${sensor.wellId}` as any); } else if (buttonIndex === 2) { // Remove handleDetachDevice(sensor); } } ); } else { // Android fallback Alert.alert( `${sensor.name} is Offline`, `Last seen: ${formatLastSeen(sensor.lastSeen)}\n\nWhat would you like to do?`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Reconnect', onPress: () => router.push(`/(tabs)/beneficiaries/${id}/setup-wifi?deviceId=${sensor.deviceId}&deviceName=${sensor.name}&wellId=${sensor.wellId}` as any) }, { text: 'Remove', style: 'destructive', onPress: () => handleDetachDevice(sensor) }, ] ); } } // For BLE nearby sensors - go directly to setup else if (sensor.source === 'ble') { router.push(`/(tabs)/beneficiaries/${id}/setup-wifi?deviceId=${sensor.deviceId}&deviceName=${sensor.name}&wellId=${sensor.wellId}` as any); } // For online API sensors - navigate to settings else { handleDeviceSettings(sensor); } }; const handleDetachDevice = (sensor: WPSensor) => { Alert.alert( 'Detach Sensor', `Are you sure you want to detach "${sensor.name}" from ${beneficiaryName}?\n\nThe sensor will become available for use with another person.`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Detach', style: 'destructive', onPress: async () => { setIsDetaching(sensor.deviceId); try { const response = await api.detachDeviceFromBeneficiary(id!, sensor.deviceId); if (!response.ok) { throw new Error('Failed to detach sensor'); } // Remove from local state setApiSensors(prev => prev.filter(s => s.deviceId !== sensor.deviceId)); Alert.alert('Success', `${sensor.name} has been detached.`); } catch (error) { console.error('[Equipment] Failed to detach sensor:', error); Alert.alert('Error', 'Failed to detach sensor. Please try again.'); } finally { setIsDetaching(null); } }, }, ] ); }; const handleDetachAll = () => { if (apiSensors.length === 0) { Alert.alert('No Sensors', 'There are no sensors to detach.'); return; } Alert.alert( 'Detach All Sensors', `Are you sure you want to detach all ${apiSensors.length} sensors from ${beneficiaryName}?\n\nThis action cannot be undone.`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Detach All', style: 'destructive', onPress: async () => { setIsLoading(true); try { // Detach all sensors sequentially for (const sensor of apiSensors) { await api.detachDeviceFromBeneficiary(id!, sensor.deviceId); } setApiSensors([]); Alert.alert('Success', 'All sensors have been detached.'); } catch (error) { console.error('[Equipment] Failed to detach all sensors:', error); Alert.alert('Error', 'Failed to detach sensors. Please try again.'); } finally { setIsLoading(false); } }, }, ] ); }; const handleAddSensor = () => { // Navigate to Add Sensor screen router.push(`/(tabs)/beneficiaries/${id}/add-sensor` as any); }; const handleDeviceSettings = (sensor: WPSensor) => { // Navigate to Device Settings screen router.push(`/(tabs)/beneficiaries/${id}/device-settings/${sensor.deviceId}` as any); }; const formatLastSeen = (lastSeen: Date): string => { const now = new Date(); const diffMs = now.getTime() - lastSeen.getTime(); const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMins < 1) return 'Just now'; if (diffMins < 60) return `${diffMins} min ago`; if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; }; const getStatusColor = (status: 'online' | 'warning' | 'offline') => { switch (status) { case 'online': return AppColors.success; case 'warning': return AppColors.warning; case 'offline': return AppColors.error; } }; const getStatusLabel = (status: 'online' | 'warning' | 'offline') => { switch (status) { case 'online': return 'Online'; case 'warning': return 'Warning'; case 'offline': return 'Offline'; } }; const getSignalStrength = (rssi: number): string => { if (rssi >= -50) return 'Excellent'; if (rssi >= -60) return 'Good'; if (rssi >= -70) return 'Fair'; return 'Weak'; }; if (isLoading) { return ( router.back()}> Sensors Loading sensors... ); } return ( {/* Header */} router.back()}> Sensors {/* Simulator Warning */} {!isBLEAvailable && ( Running in Simulator - BLE features use mock data )} } > {/* Summary Card */} {apiSensors.length} Total {apiSensors.filter(s => s.status === 'online').length} Online {apiSensors.filter(s => s.status === 'warning').length} Warning {apiSensors.filter(s => s.status === 'offline').length} Offline {/* Connected Sensors Section */} {apiSensors.length === 0 ? ( No Sensors Connected Add WP sensors to start monitoring {beneficiaryName}'s wellness. Add Sensor ) : ( <> Connected Sensors ({apiSensors.length}) {apiSensors.map((sensor) => { const isDetachingThis = isDetaching === sensor.deviceId; const sensorConfig = { icon: 'water' as const, color: AppColors.primary, bgColor: AppColors.primaryLighter, }; return ( handleSensorPress(sensor)} activeOpacity={0.7} > {sensor.name} {getStatusLabel(sensor.status)} {formatLastSeen(sensor.lastSeen)} { e.stopPropagation(); handleDeviceSettings(sensor); }} activeOpacity={0.7} > {sensor.location || 'No location set'} { e.stopPropagation(); handleDeviceSettings(sensor); }} > { e.stopPropagation(); handleDetachDevice(sensor); }} disabled={isDetachingThis} > {isDetachingThis ? ( ) : ( )} ); })} {/* Detach All Button */} {apiSensors.length > 1 && ( Detach All Sensors )} )} {/* Scan Nearby Button */} {isBLEScanning ? ( <> Scanning... ({bleSensors.length} found) ) : ( <> {bleSensors.length > 0 ? 'Scan Again' : 'Scan for Nearby Sensors'} )} {/* Available Nearby Section */} {bleSensors.length > 0 && ( <> Available Nearby ({bleSensors.length}) {bleSensors.map((sensor) => { const sensorConfig = { icon: 'water-outline' as const, color: AppColors.textMuted, bgColor: AppColors.surface, }; return ( handleSensorPress(sensor)} activeOpacity={0.7} > {sensor.name} Not Connected Tap to connect ); })} )} {/* Info Section */} About Sensors WP sensors monitor wellness metrics via WiFi. Tap a sensor to configure WiFi settings, view detailed status, or troubleshoot connectivity issues. {'\n\n'} Detaching a sensor removes it from {beneficiaryName}'s monitoring setup. You can then attach it to another person or re-attach it later. ); } 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, }, headerRight: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, }, 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, }, deviceActions: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, }, settingsButton: { width: 40, height: 40, borderRadius: BorderRadius.md, backgroundColor: AppColors.primaryLighter, justifyContent: 'center', alignItems: 'center', }, 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, }, // WiFi Info wifiInfo: { flexDirection: 'row', alignItems: 'center', gap: 4, marginTop: 4, }, wifiText: { fontSize: FontSizes.xs, color: AppColors.textMuted, }, // Simulator Warning simulatorWarning: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.warningLight, paddingVertical: Spacing.xs, paddingHorizontal: Spacing.md, gap: Spacing.xs, borderBottomWidth: 1, borderBottomColor: AppColors.warning, }, simulatorWarningText: { fontSize: FontSizes.xs, color: AppColors.warning, fontWeight: FontWeights.medium, flex: 1, }, // Scan Button scanButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: AppColors.primary, paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, marginTop: Spacing.lg, marginBottom: Spacing.lg, gap: Spacing.sm, ...Shadows.md, }, scanButtonActive: { backgroundColor: AppColors.primaryDark, }, scanButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.white, }, // Nearby Device Card nearbyDeviceCard: { borderWidth: 1, borderColor: AppColors.border, borderStyle: 'dashed', }, deviceLocation: { fontSize: FontSizes.xs, color: AppColors.textMuted, marginTop: 2, }, deviceLocationPlaceholder: { fontStyle: 'italic', opacity: 0.6, }, });