import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert, ActivityIndicator, TextInput, Modal, FlatList, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import { router, useLocalSearchParams } from 'expo-router'; import { useBLE } from '@/contexts/BLEContext'; import { api, ROOM_LOCATIONS, type RoomLocationId } from '@/services/api'; import type { WiFiStatus } from '@/services/ble'; import { BLEConnectionState } from '@/services/ble'; import { ConnectionStatusIndicator } from '@/components/ble/ConnectionStatusIndicator'; import { AppColors, BorderRadius, FontSizes, FontWeights, Spacing, Shadows, } from '@/constants/theme'; interface SensorInfo { deviceId: string; wellId: number; mac: string; name: string; status: 'online' | 'warning' | 'offline'; lastSeen: Date; beneficiaryId: string; deploymentId: number; location?: string; description?: string; } export default function DeviceSettingsScreen() { const { id, deviceId } = useLocalSearchParams<{ id: string; deviceId: string }>(); const { connectedDevices, reconnectingDevices, isBLEAvailable, connectDevice, disconnectDevice, getCurrentWiFi, rebootDevice, enableAutoReconnect, disableAutoReconnect, manualReconnect, cancelReconnect, getReconnectState, getConnectionState, } = useBLE(); const [sensorInfo, setSensorInfo] = useState(null); const [currentWiFi, setCurrentWiFi] = useState(null); const [isLoadingInfo, setIsLoadingInfo] = useState(true); const [isConnecting, setIsConnecting] = useState(false); const [isLoadingWiFi, setIsLoadingWiFi] = useState(false); const [isRebooting, setIsRebooting] = useState(false); const [isSaving, setIsSaving] = useState(false); // Editable fields const [location, setLocation] = useState(''); const [description, setDescription] = useState(''); const [showLocationPicker, setShowLocationPicker] = useState(false); const isConnected = connectedDevices.has(deviceId!); const isReconnecting = reconnectingDevices.has(deviceId!); const connectionState = getConnectionState(deviceId!); const reconnectState = getReconnectState(deviceId!); const loadSensorInfo = useCallback(async () => { setIsLoadingInfo(true); try { // Get sensor info from API const response = await api.getDevicesForBeneficiary(id!); if (!response.ok || !response.data) { throw new Error('Failed to load sensor info'); } const sensor = response.data.find((s: SensorInfo) => s.deviceId === deviceId); if (sensor) { setSensorInfo(sensor); setLocation((sensor.location as RoomLocationId) || ''); setDescription(sensor.description || ''); } else { throw new Error('Sensor not found'); } } catch { Alert.alert('Error', 'Failed to load sensor information'); router.back(); } finally { setIsLoadingInfo(false); } }, [id, deviceId]); const loadWiFiStatus = useCallback(async () => { if (!isConnected) return; setIsLoadingWiFi(true); try { const wifiStatus = await getCurrentWiFi(deviceId!); setCurrentWiFi(wifiStatus); } catch { Alert.alert('Error', 'Failed to get WiFi status'); } finally { setIsLoadingWiFi(false); } }, [isConnected, deviceId, getCurrentWiFi]); useEffect(() => { loadSensorInfo(); }, [loadSensorInfo]); const handleConnect = async () => { if (!sensorInfo) return; setIsConnecting(true); try { const success = await connectDevice(deviceId!); if (!success) { throw new Error('Connection failed'); } // Enable auto-reconnect for this device enableAutoReconnect(deviceId!, sensorInfo.name); // Load WiFi status after connecting loadWiFiStatus(); } catch { Alert.alert('Connection Failed', 'Failed to connect to sensor via Bluetooth.'); } finally { setIsConnecting(false); } }; const handleManualReconnect = useCallback(async () => { if (!sensorInfo) return; try { const success = await manualReconnect(deviceId!); if (success) { // Re-enable auto-reconnect enableAutoReconnect(deviceId!, sensorInfo.name); // Load WiFi status after reconnecting loadWiFiStatus(); } else { Alert.alert('Reconnection Failed', 'Could not reconnect to the sensor. Please move closer and try again.'); } } catch (error: any) { Alert.alert('Reconnection Failed', error.message || 'Failed to reconnect to sensor.'); } }, [deviceId, sensorInfo, manualReconnect, enableAutoReconnect, loadWiFiStatus]); const handleCancelReconnect = useCallback(() => { cancelReconnect(deviceId!); }, [deviceId, cancelReconnect]); const handleDisconnect = useCallback(async () => { // Disable auto-reconnect before disconnecting disableAutoReconnect(deviceId!); await disconnectDevice(deviceId!); }, [deviceId, disableAutoReconnect, disconnectDevice]); const handleChangeWiFi = () => { if (!isConnected) { Alert.alert('Not Connected', 'Please connect to the sensor via Bluetooth first.'); return; } // Navigate to Setup WiFi screen router.push({ pathname: `/(tabs)/beneficiaries/${id}/setup-wifi` as any, params: { deviceId: deviceId!, deviceName: sensorInfo?.name || '', wellId: sensorInfo?.wellId.toString() || '', }, }); }; const handleReboot = () => { if (!isConnected) { Alert.alert('Not Connected', 'Please connect to the sensor via Bluetooth first.'); return; } Alert.alert( 'Reboot Sensor', 'Are you sure you want to reboot this sensor? It will disconnect from Bluetooth and restart.', [ { text: 'Cancel', style: 'cancel' }, { text: 'Reboot', style: 'destructive', onPress: async () => { setIsRebooting(true); try { await rebootDevice(deviceId!); Alert.alert('Success', 'Sensor is rebooting. It will be back online in a minute.'); router.back(); } catch { Alert.alert('Error', 'Failed to reboot sensor'); } finally{ setIsRebooting(false); } }, }, ] ); }; const handleSaveMetadata = async () => { if (!sensorInfo) return; // Check if anything changed const locationChanged = location !== (sensorInfo.location || ''); const descriptionChanged = description !== (sensorInfo.description || ''); if (!locationChanged && !descriptionChanged) { Alert.alert('No Changes', 'No changes to save.'); return; } setIsSaving(true); try { const updates: { location?: string; description?: string } = {}; if (locationChanged) updates.location = location; if (descriptionChanged) updates.description = description; const response = await api.updateDeviceMetadata(sensorInfo.deviceId, updates); if (!response.ok) { throw new Error(response.error?.message || 'Failed to save'); } // Update local state setSensorInfo({ ...sensorInfo, location, description, }); Alert.alert('Success', 'Device information updated.'); } catch (error: any) { Alert.alert('Error', error.message || 'Failed to save device information'); } finally { setIsSaving(false); } }; const hasUnsavedChanges = () => { if (!sensorInfo) return false; return ( location !== (sensorInfo.location || '') || description !== (sensorInfo.description || '') ); }; 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 getSignalStrength = (rssi: number): string => { if (rssi >= -50) return 'Excellent'; if (rssi >= -60) return 'Good'; if (rssi >= -70) return 'Fair'; return 'Weak'; }; const getSignalColor = (rssi: number) => { if (rssi >= -50) return AppColors.success; if (rssi >= -60) return AppColors.info; if (rssi >= -70) return AppColors.warning; return AppColors.error; }; if (isLoadingInfo || !sensorInfo) { return ( router.back()}> Sensor Settings Loading sensor info... ); } return ( {/* Header */} router.back()}> Sensor Settings {/* Simulator Warning */} {!isBLEAvailable && ( Running in Simulator - BLE features use mock data )} {/* Sensor Info Card */} {sensorInfo.name} {sensorInfo.status === 'online' ? 'Online' : 'Offline'} {formatLastSeen(sensorInfo.lastSeen)} {/* Details Section */} Device Information Well ID {sensorInfo.wellId} MAC Address {sensorInfo.mac} Deployment ID {sensorInfo.deploymentId} {/* Editable Metadata Section */} Sensor Details Location setShowLocationPicker(true)} > {location ? `${ROOM_LOCATIONS.find(l => l.id === location)?.icon} ${ROOM_LOCATIONS.find(l => l.id === location)?.label}` : 'Select location...'} Description {hasUnsavedChanges() && ( <> {isSaving ? ( ) : ( )} {isSaving ? 'Saving...' : 'Save Changes'} )} {/* BLE Connection Section */} Bluetooth Connection {/* Connection Status Indicator */} {/* Connected state actions */} {isConnected && !isReconnecting && ( Disconnect )} {/* Not connected - show connect button */} {!isConnected && !isReconnecting && connectionState !== BLEConnectionState.CONNECTING && ( {isConnecting ? ( ) : ( )} {isConnecting ? 'Connecting...' : 'Connect via Bluetooth'} )} {/* WiFi Status Section */} {isConnected && ( WiFi Status {isLoadingWiFi ? ( Loading WiFi status... ) : currentWiFi ? ( {currentWiFi.ssid} {getSignalStrength(currentWiFi.rssi)} ({currentWiFi.rssi} dBm) Change WiFi ) : ( Not connected to WiFi Setup WiFi )} )} {/* Actions Section */} {isConnected && ( Actions Refresh WiFi Status {isRebooting ? ( ) : ( )} Reboot Sensor )} {/* Info Card */} About Settings • Connect via Bluetooth to view WiFi status and change settings{'\n'} • Make sure you're within range (about 10 meters) of the sensor{'\n'} • Rebooting will disconnect Bluetooth and restart the sensor {/* Location Picker Modal */} setShowLocationPicker(false)} > Select Location setShowLocationPicker(false)} > item.id} renderItem={({ item }) => ( { setLocation(item.id); setShowLocationPicker(false); }} > {item.icon} {item.label} {location === item.id && ( )} )} ItemSeparatorComponent={() => } /> ); } 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, }, 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, }, 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, }, // Sensor Card sensorCard: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, padding: Spacing.lg, marginBottom: Spacing.lg, ...Shadows.sm, }, sensorIcon: { width: 80, height: 80, borderRadius: BorderRadius.lg, backgroundColor: AppColors.primaryLighter, justifyContent: 'center', alignItems: 'center', marginRight: Spacing.md, }, sensorInfo: { flex: 1, }, sensorName: { fontSize: FontSizes.xl, fontWeight: FontWeights.bold, color: AppColors.textPrimary, marginBottom: Spacing.xs, }, statusRow: { flexDirection: 'row', alignItems: 'center', gap: 6, }, statusDot: { width: 8, height: 8, borderRadius: 4, }, statusText: { fontSize: FontSizes.sm, color: AppColors.textSecondary, fontWeight: FontWeights.medium, }, statusSeparator: { fontSize: FontSizes.sm, color: AppColors.textMuted, }, lastSeenText: { fontSize: FontSizes.sm, color: AppColors.textMuted, }, // Section section: { marginBottom: Spacing.lg, }, sectionTitle: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.textSecondary, marginBottom: Spacing.md, textTransform: 'uppercase', letterSpacing: 0.5, }, // Details Card detailsCard: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.md, ...Shadows.xs, }, detailRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: Spacing.sm, }, detailLabel: { fontSize: FontSizes.sm, color: AppColors.textMuted, }, detailValue: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, }, detailDivider: { height: 1, backgroundColor: AppColors.border, }, // Editable fields editableRow: { paddingVertical: Spacing.sm, }, editableLabel: { fontSize: FontSizes.sm, color: AppColors.textMuted, marginBottom: Spacing.xs, }, editableInput: { fontSize: FontSizes.base, color: AppColors.textPrimary, backgroundColor: AppColors.background, borderRadius: BorderRadius.md, paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, borderWidth: 1, borderColor: AppColors.border, }, editableInputMultiline: { minHeight: 60, textAlignVertical: 'top', }, // Picker Button pickerButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: AppColors.background, borderRadius: BorderRadius.md, paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, borderWidth: 1, borderColor: AppColors.border, minHeight: 44, }, pickerButtonText: { fontSize: FontSizes.base, color: AppColors.textPrimary, }, pickerButtonPlaceholder: { color: AppColors.textMuted, }, saveButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: AppColors.primary, paddingVertical: Spacing.sm, paddingHorizontal: Spacing.md, borderRadius: BorderRadius.md, marginTop: Spacing.sm, gap: Spacing.xs, }, saveButtonText: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.white, }, // BLE Connection connectedActions: { marginTop: Spacing.md, }, disconnectButton: { paddingVertical: Spacing.sm, paddingHorizontal: Spacing.md, borderRadius: BorderRadius.md, borderWidth: 1, borderColor: AppColors.border, alignItems: 'center', }, disconnectButtonText: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.textSecondary, }, connectButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: AppColors.primary, paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, gap: Spacing.sm, marginTop: Spacing.md, ...Shadows.sm, }, connectButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.white, }, // WiFi Status loadingWiFiCard: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md, backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.md, ...Shadows.xs, }, loadingWiFiText: { fontSize: FontSizes.sm, color: AppColors.textSecondary, }, wifiCard: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.md, ...Shadows.xs, }, wifiHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.md, marginBottom: Spacing.md, }, wifiInfo: { flex: 1, }, wifiSSID: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, marginBottom: 2, }, wifiSignal: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, }, changeWiFiButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: Spacing.xs, paddingVertical: Spacing.sm, paddingHorizontal: Spacing.md, borderRadius: BorderRadius.md, borderWidth: 1, borderColor: AppColors.primary, }, changeWiFiText: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.primary, }, noWiFiCard: { alignItems: 'center', backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.xl, ...Shadows.xs, }, noWiFiText: { fontSize: FontSizes.base, color: AppColors.textMuted, marginTop: Spacing.md, marginBottom: Spacing.md, }, setupWiFiButton: { paddingVertical: Spacing.sm, paddingHorizontal: Spacing.lg, }, setupWiFiText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.primary, }, // Actions actionsCard: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.md, ...Shadows.xs, }, actionButton: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, paddingVertical: Spacing.sm, }, actionButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.medium, color: AppColors.primary, }, actionDivider: { height: 1, backgroundColor: AppColors.border, marginVertical: Spacing.sm, }, // Info Card infoCard: { backgroundColor: AppColors.infoLight, borderRadius: BorderRadius.lg, padding: Spacing.md, }, 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, }, // Modal styles modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'flex-end', }, modalContent: { backgroundColor: AppColors.surface, borderTopLeftRadius: BorderRadius.xl, borderTopRightRadius: BorderRadius.xl, maxHeight: '70%', paddingBottom: Spacing.xl, }, modalHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md, borderBottomWidth: 1, borderBottomColor: AppColors.border, }, modalTitle: { fontSize: FontSizes.lg, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, }, modalCloseButton: { padding: Spacing.xs, }, locationOption: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: Spacing.lg, paddingVertical: Spacing.md, gap: Spacing.md, }, locationOptionSelected: { backgroundColor: AppColors.primaryLighter, }, locationOptionIcon: { fontSize: 24, width: 32, textAlign: 'center', }, locationOptionText: { flex: 1, fontSize: FontSizes.base, color: AppColors.textPrimary, }, locationOptionTextSelected: { fontWeight: FontWeights.semibold, color: AppColors.primary, }, locationSeparator: { height: 1, backgroundColor: AppColors.border, marginLeft: Spacing.lg + 32 + Spacing.md, }, });