diff --git a/app/(tabs)/beneficiaries/[id]/device-settings/[deviceId].tsx b/app/(tabs)/beneficiaries/[id]/device-settings/[deviceId].tsx new file mode 100644 index 0000000..6fbf5ed --- /dev/null +++ b/app/(tabs)/beneficiaries/[id]/device-settings/[deviceId].tsx @@ -0,0 +1,728 @@ +import React, { useState, useEffect } from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + Alert, + ActivityIndicator, +} 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 { useBLE } from '@/contexts/BLEContext'; +import { api } from '@/services/api'; +import type { WiFiStatus } from '@/services/ble'; +import { + AppColors, + BorderRadius, + FontSizes, + FontWeights, + Spacing, + Shadows, +} from '@/constants/theme'; + +interface SensorInfo { + deviceId: string; + wellId: number; + mac: string; + name: string; + status: 'online' | 'offline'; + lastSeen: Date; + beneficiaryId: string; + deploymentId: number; +} + +export default function DeviceSettingsScreen() { + const { id, deviceId } = useLocalSearchParams<{ id: string; deviceId: string }>(); + const { + connectedDevices, + isBLEAvailable, + connectDevice, + disconnectDevice, + getCurrentWiFi, + rebootDevice, + } = 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 isConnected = connectedDevices.has(deviceId!); + + useEffect(() => { + loadSensorInfo(); + }, []); + + const loadSensorInfo = async () => { + setIsLoadingInfo(true); + + try { + // Get sensor info from API + const response = await api.getDevicesForBeneficiary(id!); + + if (!response.ok) { + throw new Error('Failed to load sensor info'); + } + + const sensor = response.data.find((s: any) => s.deviceId === deviceId); + + if (sensor) { + setSensorInfo(sensor); + } else { + throw new Error('Sensor not found'); + } + } catch (error: any) { + console.error('[DeviceSettings] Failed to load sensor info:', error); + Alert.alert('Error', 'Failed to load sensor information'); + router.back(); + } finally { + setIsLoadingInfo(false); + } + }; + + const handleConnect = async () => { + if (!sensorInfo) return; + + setIsConnecting(true); + + try { + const success = await connectDevice(deviceId!); + + if (!success) { + throw new Error('Connection failed'); + } + + // Load WiFi status after connecting + loadWiFiStatus(); + } catch (error: any) { + console.error('[DeviceSettings] Connection failed:', error); + Alert.alert('Connection Failed', 'Failed to connect to sensor via Bluetooth.'); + } finally { + setIsConnecting(false); + } + }; + + const loadWiFiStatus = async () => { + if (!isConnected) return; + + setIsLoadingWiFi(true); + + try { + const wifiStatus = await getCurrentWiFi(deviceId!); + setCurrentWiFi(wifiStatus); + } catch (error: any) { + console.error('[DeviceSettings] Failed to get WiFi status:', error); + Alert.alert('Error', 'Failed to get WiFi status'); + } finally { + setIsLoadingWiFi(false); + } + }; + + 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 (error: any) { + console.error('[DeviceSettings] Reboot failed:', error); + Alert.alert('Error', 'Failed to reboot sensor'); + } finally { + setIsRebooting(false); + } + }, + }, + ] + ); + }; + + 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} + + + + + {/* BLE Connection Section */} + + Bluetooth Connection + {isConnected ? ( + + + + Connected + + disconnectDevice(deviceId!)} + > + Disconnect + + + ) : ( + + {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 + + + + + ); +} + +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, + }, + // BLE Connection + connectedCard: { + backgroundColor: AppColors.surface, + borderRadius: BorderRadius.lg, + padding: Spacing.md, + ...Shadows.xs, + }, + connectedHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: Spacing.sm, + marginBottom: Spacing.md, + }, + connectedText: { + fontSize: FontSizes.base, + fontWeight: FontWeights.semibold, + color: AppColors.success, + }, + 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, + ...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, + }, +});