Add device settings screen

Features:
- Device metadata display (name, MAC, location, description)
- Edit device name and description
- Update WiFi credentials (reconnect flow)
- Remove device from beneficiary
- Device history and diagnostics

UI:
- Clean settings form with validation
- Delete confirmation dialog
- Success/error feedback
- Navigation back to equipment list on changes

Route: /(tabs)/beneficiaries/[id]/device-settings/[deviceId]
This commit is contained in:
Sergei 2026-01-14 19:08:09 -08:00
parent 3c3283e424
commit 5092678430

View File

@ -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<SensorInfo | null>(null);
const [currentWiFi, setCurrentWiFi] = useState<WiFiStatus | null>(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 (
<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}>Sensor Settings</Text>
<View style={styles.placeholder} />
</View>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={AppColors.primary} />
<Text style={styles.loadingText}>Loading sensor info...</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}>Sensor Settings</Text>
<View style={styles.placeholder} />
</View>
{/* Simulator Warning */}
{!isBLEAvailable && (
<View style={styles.simulatorWarning}>
<Ionicons name="information-circle" size={18} color={AppColors.warning} />
<Text style={styles.simulatorWarningText}>
Running in Simulator - BLE features use mock data
</Text>
</View>
)}
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
{/* Sensor Info Card */}
<View style={styles.sensorCard}>
<View style={styles.sensorIcon}>
<Ionicons name="water" size={48} color={AppColors.primary} />
</View>
<View style={styles.sensorInfo}>
<Text style={styles.sensorName}>{sensorInfo.name}</Text>
<View style={styles.statusRow}>
<View
style={[
styles.statusDot,
{
backgroundColor:
sensorInfo.status === 'online' ? AppColors.success : AppColors.error,
},
]}
/>
<Text style={styles.statusText}>
{sensorInfo.status === 'online' ? 'Online' : 'Offline'}
</Text>
<Text style={styles.statusSeparator}></Text>
<Text style={styles.lastSeenText}>{formatLastSeen(sensorInfo.lastSeen)}</Text>
</View>
</View>
</View>
{/* Details Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Device Information</Text>
<View style={styles.detailsCard}>
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Well ID</Text>
<Text style={styles.detailValue}>{sensorInfo.wellId}</Text>
</View>
<View style={styles.detailDivider} />
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>MAC Address</Text>
<Text style={styles.detailValue}>{sensorInfo.mac}</Text>
</View>
<View style={styles.detailDivider} />
<View style={styles.detailRow}>
<Text style={styles.detailLabel}>Deployment ID</Text>
<Text style={styles.detailValue}>{sensorInfo.deploymentId}</Text>
</View>
</View>
</View>
{/* BLE Connection Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>Bluetooth Connection</Text>
{isConnected ? (
<View style={styles.connectedCard}>
<View style={styles.connectedHeader}>
<Ionicons name="bluetooth" size={24} color={AppColors.success} />
<Text style={styles.connectedText}>Connected</Text>
</View>
<TouchableOpacity
style={styles.disconnectButton}
onPress={() => disconnectDevice(deviceId!)}
>
<Text style={styles.disconnectButtonText}>Disconnect</Text>
</TouchableOpacity>
</View>
) : (
<TouchableOpacity
style={styles.connectButton}
onPress={handleConnect}
disabled={isConnecting}
>
{isConnecting ? (
<ActivityIndicator size="small" color={AppColors.white} />
) : (
<Ionicons name="bluetooth" size={20} color={AppColors.white} />
)}
<Text style={styles.connectButtonText}>
{isConnecting ? 'Connecting...' : 'Connect via Bluetooth'}
</Text>
</TouchableOpacity>
)}
</View>
{/* WiFi Status Section */}
{isConnected && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>WiFi Status</Text>
{isLoadingWiFi ? (
<View style={styles.loadingWiFiCard}>
<ActivityIndicator size="small" color={AppColors.primary} />
<Text style={styles.loadingWiFiText}>Loading WiFi status...</Text>
</View>
) : currentWiFi ? (
<View style={styles.wifiCard}>
<View style={styles.wifiHeader}>
<Ionicons name="wifi" size={24} color={AppColors.success} />
<View style={styles.wifiInfo}>
<Text style={styles.wifiSSID}>{currentWiFi.ssid}</Text>
<Text
style={[styles.wifiSignal, { color: getSignalColor(currentWiFi.rssi) }]}
>
{getSignalStrength(currentWiFi.rssi)} ({currentWiFi.rssi} dBm)
</Text>
</View>
</View>
<TouchableOpacity style={styles.changeWiFiButton} onPress={handleChangeWiFi}>
<Ionicons name="settings-outline" size={18} color={AppColors.primary} />
<Text style={styles.changeWiFiText}>Change WiFi</Text>
</TouchableOpacity>
</View>
) : (
<View style={styles.noWiFiCard}>
<Ionicons name="wifi-outline" size={32} color={AppColors.textMuted} />
<Text style={styles.noWiFiText}>Not connected to WiFi</Text>
<TouchableOpacity style={styles.setupWiFiButton} onPress={handleChangeWiFi}>
<Text style={styles.setupWiFiText}>Setup WiFi</Text>
</TouchableOpacity>
</View>
)}
</View>
)}
{/* Actions Section */}
{isConnected && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Actions</Text>
<View style={styles.actionsCard}>
<TouchableOpacity
style={styles.actionButton}
onPress={loadWiFiStatus}
disabled={isLoadingWiFi}
>
<Ionicons name="refresh" size={20} color={AppColors.primary} />
<Text style={styles.actionButtonText}>Refresh WiFi Status</Text>
</TouchableOpacity>
<View style={styles.actionDivider} />
<TouchableOpacity
style={styles.actionButton}
onPress={handleReboot}
disabled={isRebooting}
>
{isRebooting ? (
<ActivityIndicator size="small" color={AppColors.warning} />
) : (
<Ionicons name="power" size={20} color={AppColors.warning} />
)}
<Text style={[styles.actionButtonText, { color: AppColors.warning }]}>
Reboot Sensor
</Text>
</TouchableOpacity>
</View>
</View>
)}
{/* Info Card */}
<View style={styles.infoCard}>
<View style={styles.infoHeader}>
<Ionicons name="information-circle" size={20} color={AppColors.info} />
<Text style={styles.infoTitle}>About Settings</Text>
</View>
<Text style={styles.infoText}>
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
</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,
},
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,
},
});