Implemented three-tier sensor status (online/warning/offline) with visual indicators and BLE scanning for nearby devices. Features: - WPSensor type with status field (online/warning/offline) - Automatic status calculation based on lastSeen time: • Online: < 5 minutes (fresh data) • Warning: 5 min - 1 hour (potential issue) • Offline: > 1 hour (definitely problem) - Dual sensor display: Connected (API) + Available Nearby (BLE) - BLE scanning button for discovering nearby WP sensors - Action Sheet for offline sensors with Reconnect/Remove options - Updated summary card: Total/Online/Warning/Offline counts - Visual status indicators: colored dots and labels - Graceful error handling for API unavailability Files changed: - types/index.ts: Added WPSensor interface with status and source fields - services/api.ts: Updated getDevicesForBeneficiary with status calculation - equipment.tsx: Complete UI overhaul with BLE scanning and two-tier sensor list 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
871 lines
27 KiB
TypeScript
871 lines
27 KiB
TypeScript
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, scanForDevices, stopScan } = useBLE();
|
|
|
|
// Separate state for API sensors (attached) and BLE sensors (nearby)
|
|
const [apiSensors, setApiSensors] = useState<WPSensor[]>([]);
|
|
const [bleSensors, setBleSensors] = useState<WPSensor[]>([]);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
const [isScanning, setIsScanning] = useState(false);
|
|
const [isDetaching, setIsDetaching] = useState<string | null>(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 (isScanning) {
|
|
// Stop scan
|
|
stopScan();
|
|
setIsScanning(false);
|
|
return;
|
|
}
|
|
|
|
setIsScanning(true);
|
|
setBleSensors([]); // Clear previous results
|
|
|
|
try {
|
|
const devices = await scanForDevices(10000); // 10 second scan
|
|
|
|
// Convert BLE devices to WPSensor format
|
|
const nearbyWPSensors: WPSensor[] = devices
|
|
.filter(d => d.name?.startsWith('WP_')) // Only WP sensors
|
|
.map(d => {
|
|
// Parse WP_<wellId>_<mac> 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);
|
|
} catch (error) {
|
|
console.error('[Equipment] BLE scan failed:', error);
|
|
Alert.alert('Scan Failed', 'Could not scan for nearby sensors. Make sure Bluetooth is enabled.');
|
|
} finally {
|
|
setIsScanning(false);
|
|
}
|
|
};
|
|
|
|
// 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 (
|
|
<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}>Sensors</Text>
|
|
<View style={styles.placeholder} />
|
|
</View>
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator size="large" color={AppColors.primary} />
|
|
<Text style={styles.loadingText}>Loading sensors...</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}>Sensors</Text>
|
|
<View style={styles.headerRight}>
|
|
<TouchableOpacity style={styles.addButton} onPress={handleAddSensor}>
|
|
<Ionicons name="add" size={24} color={AppColors.primary} />
|
|
</TouchableOpacity>
|
|
<BeneficiaryMenu
|
|
beneficiaryId={id || ''}
|
|
userRole={currentBeneficiary?.role}
|
|
currentPage="sensors"
|
|
/>
|
|
</View>
|
|
</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}
|
|
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}>{apiSensors.length}</Text>
|
|
<Text style={styles.summaryLabel}>Total</Text>
|
|
</View>
|
|
<View style={styles.summaryDivider} />
|
|
<View style={styles.summaryItem}>
|
|
<Text style={[styles.summaryValue, { color: AppColors.success }]}>
|
|
{apiSensors.filter(s => s.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.warning }]}>
|
|
{apiSensors.filter(s => s.status === 'warning').length}
|
|
</Text>
|
|
<Text style={styles.summaryLabel}>Warning</Text>
|
|
</View>
|
|
<View style={styles.summaryDivider} />
|
|
<View style={styles.summaryItem}>
|
|
<Text style={[styles.summaryValue, { color: AppColors.error }]}>
|
|
{apiSensors.filter(s => s.status === 'offline').length}
|
|
</Text>
|
|
<Text style={styles.summaryLabel}>Offline</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Connected Sensors Section */}
|
|
{apiSensors.length === 0 ? (
|
|
<View style={styles.emptyState}>
|
|
<View style={styles.emptyIconContainer}>
|
|
<Ionicons name="water-outline" size={48} color={AppColors.textMuted} />
|
|
</View>
|
|
<Text style={styles.emptyTitle}>No Sensors Connected</Text>
|
|
<Text style={styles.emptyText}>
|
|
Add WP sensors to start monitoring {beneficiaryName}'s wellness.
|
|
</Text>
|
|
<TouchableOpacity style={styles.addDeviceButton} onPress={handleAddSensor}>
|
|
<Ionicons name="add" size={20} color={AppColors.white} />
|
|
<Text style={styles.addDeviceButtonText}>Add Sensor</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
) : (
|
|
<>
|
|
<Text style={styles.sectionTitle}>Connected Sensors ({apiSensors.length})</Text>
|
|
<View style={styles.devicesList}>
|
|
{apiSensors.map((sensor) => {
|
|
const isDetachingThis = isDetaching === sensor.deviceId;
|
|
const sensorConfig = {
|
|
icon: 'water' as const,
|
|
color: AppColors.primary,
|
|
bgColor: AppColors.primaryLighter,
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
key={sensor.deviceId}
|
|
style={styles.deviceCard}
|
|
onPress={() => handleSensorPress(sensor)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View style={styles.deviceInfo}>
|
|
<View style={[styles.deviceIcon, { backgroundColor: sensorConfig.bgColor }]}>
|
|
<Ionicons name={sensorConfig.icon} size={22} color={sensorConfig.color} />
|
|
</View>
|
|
<View style={styles.deviceDetails}>
|
|
<Text style={styles.deviceName}>{sensor.name}</Text>
|
|
<View style={styles.deviceMeta}>
|
|
<View style={[
|
|
styles.statusDot,
|
|
{ backgroundColor: getStatusColor(sensor.status) }
|
|
]} />
|
|
<Text style={[styles.deviceStatus, { color: getStatusColor(sensor.status) }]}>
|
|
{getStatusLabel(sensor.status)}
|
|
</Text>
|
|
<Text style={styles.deviceMetaSeparator}>•</Text>
|
|
<Text style={styles.deviceRoom}>{formatLastSeen(sensor.lastSeen)}</Text>
|
|
</View>
|
|
{sensor.location && (
|
|
<Text style={styles.deviceLocation}>{sensor.location}</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
<TouchableOpacity
|
|
style={styles.detachButton}
|
|
onPress={(e) => {
|
|
e.stopPropagation();
|
|
handleDetachDevice(sensor);
|
|
}}
|
|
disabled={isDetachingThis}
|
|
>
|
|
{isDetachingThis ? (
|
|
<ActivityIndicator size="small" color={AppColors.error} />
|
|
) : (
|
|
<Ionicons name="unlink-outline" size={20} color={AppColors.error} />
|
|
)}
|
|
</TouchableOpacity>
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
|
|
{/* Detach All Button */}
|
|
{apiSensors.length > 1 && (
|
|
<TouchableOpacity style={styles.detachAllButton} onPress={handleDetachAll}>
|
|
<Ionicons name="trash-outline" size={20} color={AppColors.error} />
|
|
<Text style={styles.detachAllText}>Detach All Sensors</Text>
|
|
</TouchableOpacity>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{/* Scan Nearby Button */}
|
|
<TouchableOpacity
|
|
style={[styles.scanButton, isScanning && styles.scanButtonActive]}
|
|
onPress={handleScanNearby}
|
|
disabled={!isBLEAvailable}
|
|
>
|
|
{isScanning ? (
|
|
<>
|
|
<ActivityIndicator size="small" color={AppColors.white} />
|
|
<Text style={styles.scanButtonText}>Scanning... ({bleSensors.length} found)</Text>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Ionicons name="bluetooth" size={20} color={AppColors.white} />
|
|
<Text style={styles.scanButtonText}>
|
|
{bleSensors.length > 0 ? 'Scan Again' : 'Scan for Nearby Sensors'}
|
|
</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
{/* Available Nearby Section */}
|
|
{bleSensors.length > 0 && (
|
|
<>
|
|
<Text style={styles.sectionTitle}>Available Nearby ({bleSensors.length})</Text>
|
|
<View style={styles.devicesList}>
|
|
{bleSensors.map((sensor) => {
|
|
const sensorConfig = {
|
|
icon: 'water-outline' as const,
|
|
color: AppColors.textMuted,
|
|
bgColor: AppColors.surface,
|
|
};
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
key={sensor.deviceId}
|
|
style={[styles.deviceCard, styles.nearbyDeviceCard]}
|
|
onPress={() => handleSensorPress(sensor)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<View style={styles.deviceInfo}>
|
|
<View style={[styles.deviceIcon, { backgroundColor: sensorConfig.bgColor }]}>
|
|
<Ionicons name={sensorConfig.icon} size={22} color={sensorConfig.color} />
|
|
</View>
|
|
<View style={styles.deviceDetails}>
|
|
<Text style={styles.deviceName}>{sensor.name}</Text>
|
|
<Text style={styles.deviceMeta}>
|
|
<Text style={styles.deviceStatus}>Not Connected</Text>
|
|
<Text style={styles.deviceMetaSeparator}> • </Text>
|
|
<Text style={styles.deviceRoom}>Tap to connect</Text>
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<Ionicons name="chevron-forward" size={20} color={AppColors.textMuted} />
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</View>
|
|
</>
|
|
)}
|
|
|
|
{/* Info Section */}
|
|
<View style={styles.infoCard}>
|
|
<View style={styles.infoHeader}>
|
|
<Ionicons name="information-circle" size={20} color={AppColors.info} />
|
|
<Text style={styles.infoTitle}>About Sensors</Text>
|
|
</View>
|
|
<Text style={styles.infoText}>
|
|
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.
|
|
</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,
|
|
},
|
|
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,
|
|
},
|
|
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.secondary,
|
|
},
|
|
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,
|
|
},
|
|
});
|