From be1c2eb7f532ca56141191b731ffce35f6f0ff50 Mon Sep 17 00:00:00 2001 From: Sergei Date: Mon, 19 Jan 2026 22:47:48 -0800 Subject: [PATCH] Refactor Setup WiFi screen for batch sensor processing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add SensorSetupState and BatchSetupState types for tracking sensor setup progress - Create BatchSetupProgress component with step-by-step progress UI - Implement sequential sensor processing with: - Connect → Unlock → Set WiFi → Attach → Reboot steps - Error handling with Retry/Skip options for each sensor - Pause on failure, resume on retry/skip - Cancel all functionality - Add results screen showing success/failed sensors - Support processing multiple sensors with same WiFi credentials šŸ¤– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/(tabs)/beneficiaries/[id]/setup-wifi.tsx | 623 ++++++++++++++++--- components/BatchSetupProgress.tsx | 394 ++++++++++++ types/index.ts | 57 ++ 3 files changed, 996 insertions(+), 78 deletions(-) create mode 100644 components/BatchSetupProgress.tsx diff --git a/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx b/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx index daa15d7..21507a1 100644 --- a/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx +++ b/app/(tabs)/beneficiaries/[id]/setup-wifi.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, @@ -16,6 +16,12 @@ import * as Device from 'expo-device'; import { useBLE } from '@/contexts/BLEContext'; import { api } from '@/services/api'; import type { WiFiNetwork } from '@/services/ble'; +import type { + SensorSetupState, + SensorSetupStep, + SensorSetupStatus, +} from '@/types'; +import BatchSetupProgress from '@/components/BatchSetupProgress'; import { AppColors, BorderRadius, @@ -33,13 +39,44 @@ interface DeviceParam { wellId?: number; } +type SetupPhase = 'wifi_selection' | 'batch_setup' | 'results'; + +// Initialize steps for a sensor +function createInitialSteps(): SensorSetupStep[] { + return [ + { name: 'connect', status: 'pending' }, + { name: 'unlock', status: 'pending' }, + { name: 'wifi', status: 'pending' }, + { name: 'attach', status: 'pending' }, + { name: 'reboot', status: 'pending' }, + ]; +} + +// Initialize sensor state +function createSensorState(device: DeviceParam): SensorSetupState { + return { + deviceId: device.id, + deviceName: device.name, + wellId: device.wellId, + mac: device.mac, + status: 'pending', + steps: createInitialSteps(), + }; +} + export default function SetupWiFiScreen() { const { id, devices: devicesParam } = useLocalSearchParams<{ id: string; devices: string; // JSON string of DeviceParam[] }>(); - const { getWiFiList, setWiFi, disconnectDevice } = useBLE(); + const { + getWiFiList, + setWiFi, + connectDevice, + disconnectDevice, + rebootDevice, + } = useBLE(); // Parse devices from navigation params const selectedDevices: DeviceParam[] = React.useMemo(() => { @@ -52,28 +89,37 @@ export default function SetupWiFiScreen() { } }, [devicesParam]); - // Use first device for WiFi scanning (all devices will use same WiFi) + // Use first device for WiFi scanning const firstDevice = selectedDevices[0]; const deviceId = firstDevice?.id; - const deviceName = firstDevice?.name; - const wellId = firstDevice?.wellId?.toString(); + // UI Phase + const [phase, setPhase] = useState('wifi_selection'); + + // WiFi selection state const [networks, setNetworks] = useState([]); const [isLoadingNetworks, setIsLoadingNetworks] = useState(false); const [selectedNetwork, setSelectedNetwork] = useState(null); const [password, setPassword] = useState(''); const [showPassword, setShowPassword] = useState(false); - const [isConnecting, setIsConnecting] = useState(false); + + // Batch setup state + const [sensors, setSensors] = useState([]); + const [currentIndex, setCurrentIndex] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const setupInProgressRef = useRef(false); + const shouldCancelRef = useRef(false); useEffect(() => { loadWiFiNetworks(); }, []); const loadWiFiNetworks = async () => { + if (!deviceId) return; setIsLoadingNetworks(true); try { - const wifiList = await getWiFiList(deviceId!); + const wifiList = await getWiFiList(deviceId); setNetworks(wifiList); } catch (error: any) { console.error('[SetupWiFi] Failed to get WiFi list:', error); @@ -88,71 +134,296 @@ export default function SetupWiFiScreen() { setPassword(''); }; - const handleConnect = async () => { + // Update a specific step for a sensor + const updateSensorStep = useCallback(( + deviceId: string, + stepName: SensorSetupStep['name'], + stepStatus: SensorSetupStep['status'], + error?: string + ) => { + setSensors(prev => prev.map(sensor => { + if (sensor.deviceId !== deviceId) return sensor; + return { + ...sensor, + steps: sensor.steps.map(step => + step.name === stepName + ? { ...step, status: stepStatus, error } + : step + ), + }; + })); + }, []); + + // Update sensor status + const updateSensorStatus = useCallback(( + deviceId: string, + status: SensorSetupStatus, + error?: string + ) => { + setSensors(prev => prev.map(sensor => + sensor.deviceId === deviceId + ? { ...sensor, status, error, endTime: Date.now() } + : sensor + )); + }, []); + + // Process a single sensor + const processSensor = useCallback(async ( + sensor: SensorSetupState, + ssid: string, + pwd: string + ): Promise => { + const { deviceId, wellId, deviceName } = sensor; + const isSimulator = !Device.isDevice; + + console.log(`[SetupWiFi] [${deviceName}] Starting setup...`); + + try { + // Step 1: Connect + updateSensorStep(deviceId, 'connect', 'in_progress'); + updateSensorStatus(deviceId, 'connecting'); + const connected = await connectDevice(deviceId); + if (!connected) throw new Error('Could not connect to sensor'); + updateSensorStep(deviceId, 'connect', 'completed'); + + if (shouldCancelRef.current) return false; + + // Step 2: Unlock (PIN is handled by connectDevice in BLE manager) + updateSensorStep(deviceId, 'unlock', 'in_progress'); + updateSensorStatus(deviceId, 'unlocking'); + // PIN unlock is automatic in connectDevice, mark as completed + updateSensorStep(deviceId, 'unlock', 'completed'); + + if (shouldCancelRef.current) return false; + + // Step 3: Set WiFi + updateSensorStep(deviceId, 'wifi', 'in_progress'); + updateSensorStatus(deviceId, 'setting_wifi'); + const wifiSuccess = await setWiFi(deviceId, ssid, pwd); + if (!wifiSuccess) throw new Error('Failed to configure WiFi'); + updateSensorStep(deviceId, 'wifi', 'completed'); + + if (shouldCancelRef.current) return false; + + // Step 4: Attach to deployment via API + updateSensorStep(deviceId, 'attach', 'in_progress'); + updateSensorStatus(deviceId, 'attaching'); + + if (!isSimulator && wellId) { + const attachResponse = await api.attachDeviceToBeneficiary( + id!, + wellId, + ssid, + pwd + ); + if (!attachResponse.ok) { + throw new Error('Failed to register sensor'); + } + } else { + console.log(`[SetupWiFi] [${deviceName}] Simulator mode - skipping API attach`); + } + updateSensorStep(deviceId, 'attach', 'completed'); + + if (shouldCancelRef.current) return false; + + // Step 5: Reboot + updateSensorStep(deviceId, 'reboot', 'in_progress'); + updateSensorStatus(deviceId, 'rebooting'); + await rebootDevice(deviceId); + updateSensorStep(deviceId, 'reboot', 'completed'); + + // Success! + updateSensorStatus(deviceId, 'success'); + console.log(`[SetupWiFi] [${deviceName}] Setup completed successfully`); + return true; + + } catch (error: any) { + console.error(`[SetupWiFi] [${deviceName}] Setup failed:`, error); + const errorMsg = error.message || 'Unknown error'; + + // Find current step and mark as failed + setSensors(prev => prev.map(s => { + if (s.deviceId !== deviceId) return s; + const currentStep = s.steps.find(step => step.status === 'in_progress'); + return { + ...s, + status: 'error' as SensorSetupStatus, + error: errorMsg, + steps: s.steps.map(step => + step.status === 'in_progress' + ? { ...step, status: 'failed' as const, error: errorMsg } + : step + ), + }; + })); + + // Disconnect on error + try { + await disconnectDevice(deviceId); + } catch (e) { + // Ignore disconnect errors + } + + return false; + } + }, [ + id, connectDevice, disconnectDevice, setWiFi, rebootDevice, + updateSensorStep, updateSensorStatus + ]); + + // Run batch setup sequentially + const runBatchSetup = useCallback(async () => { + if (setupInProgressRef.current) return; + setupInProgressRef.current = true; + shouldCancelRef.current = false; + + const ssid = selectedNetwork!.ssid; + const pwd = password; + + for (let i = currentIndex; i < sensors.length; i++) { + if (shouldCancelRef.current) { + console.log('[SetupWiFi] Batch setup cancelled'); + break; + } + + setCurrentIndex(i); + const sensor = sensors[i]; + + // Skip already processed sensors + if (sensor.status === 'success' || sensor.status === 'skipped') { + continue; + } + + // If sensor has error and we're not retrying, pause + if (sensor.status === 'error' && isPaused) { + break; + } + + // Reset sensor state if retrying + if (sensor.status === 'error') { + setSensors(prev => prev.map(s => + s.deviceId === sensor.deviceId + ? { ...s, status: 'pending' as SensorSetupStatus, error: undefined, steps: createInitialSteps() } + : s + )); + } + + const success = await processSensor( + sensors[i], + ssid, + pwd + ); + + // Check for cancellation after each sensor + if (shouldCancelRef.current) break; + + // If failed, pause for user input + if (!success) { + setIsPaused(true); + setupInProgressRef.current = false; + return; + } + } + + // All done + setupInProgressRef.current = false; + + // Check if we should show results + const finalSensors = sensors; + const allProcessed = finalSensors.every( + s => s.status === 'success' || s.status === 'error' || s.status === 'skipped' + ); + + if (allProcessed || shouldCancelRef.current) { + setPhase('results'); + } + }, [sensors, currentIndex, selectedNetwork, password, isPaused, processSensor]); + + // Start batch setup + const handleStartBatchSetup = () => { if (!selectedNetwork) { Alert.alert('Error', 'Please select a WiFi network'); return; } - if (!password) { Alert.alert('Error', 'Please enter WiFi password'); return; } - setIsConnecting(true); + // Initialize sensor states + const initialStates = selectedDevices.map(createSensorState); + setSensors(initialStates); + setCurrentIndex(0); + setIsPaused(false); + setPhase('batch_setup'); + }; - try { - // Step 1: Set WiFi on the device via BLE - const success = await setWiFi(deviceId!, selectedNetwork.ssid, password); - - if (!success) { - throw new Error('Failed to configure WiFi on sensor'); - } - - // Step 2: Attach device to beneficiary via API (skip in simulator/mock mode) - const isSimulator = !Device.isDevice; - - if (!isSimulator) { - const attachResponse = await api.attachDeviceToBeneficiary( - id!, - parseInt(wellId!, 10), - selectedNetwork.ssid, - password - ); - - if (!attachResponse.ok) { - throw new Error('Failed to attach sensor to beneficiary'); - } - } else { - console.log('[SetupWiFi] Simulator mode - skipping API attach'); - } - - // Step 3: Disconnect BLE connection (sensor will reboot and connect to WiFi) - await disconnectDevice(deviceId!); - - // Success! - Alert.alert( - 'Success!', - `${deviceName} has been configured and attached.\n\nThe sensor will now reboot and connect to "${selectedNetwork.ssid}". This may take a minute.`, - [ - { - text: 'Done', - onPress: () => { - // Navigate back to Equipment screen - router.replace(`/(tabs)/beneficiaries/${id}/equipment` as any); - }, - }, - ] - ); - } catch (error: any) { - console.error('[SetupWiFi] Failed to connect:', error); - Alert.alert( - 'Connection Failed', - error.message || 'Failed to configure WiFi. Please check the password and try again.' - ); - } finally { - setIsConnecting(false); + // Start processing after phase change + useEffect(() => { + if (phase === 'batch_setup' && sensors.length > 0 && !setupInProgressRef.current) { + runBatchSetup(); } + }, [phase, sensors.length, runBatchSetup]); + + // Retry failed sensor + const handleRetry = (deviceId: string) => { + const index = sensors.findIndex(s => s.deviceId === deviceId); + if (index >= 0) { + setSensors(prev => prev.map(s => + s.deviceId === deviceId + ? { ...s, status: 'pending' as SensorSetupStatus, error: undefined, steps: createInitialSteps() } + : s + )); + setCurrentIndex(index); + setIsPaused(false); + runBatchSetup(); + } + }; + + // Skip failed sensor + const handleSkip = (deviceId: string) => { + setSensors(prev => prev.map(s => + s.deviceId === deviceId + ? { ...s, status: 'skipped' as SensorSetupStatus } + : s + )); + setIsPaused(false); + + // Move to next sensor + const nextIndex = currentIndex + 1; + if (nextIndex < sensors.length) { + setCurrentIndex(nextIndex); + runBatchSetup(); + } else { + setPhase('results'); + } + }; + + // Cancel all + const handleCancelAll = () => { + Alert.alert( + 'Cancel Setup', + 'Are you sure you want to cancel? Progress will be lost.', + [ + { text: 'Continue Setup', style: 'cancel' }, + { + text: 'Cancel', + style: 'destructive', + onPress: () => { + shouldCancelRef.current = true; + setupInProgressRef.current = false; + // Disconnect all devices + selectedDevices.forEach(d => disconnectDevice(d.id)); + router.back(); + }, + }, + ] + ); + }; + + // Done - navigate back + const handleDone = () => { + router.replace(`/(tabs)/beneficiaries/${id}/equipment` as any); }; const getSignalStrength = (rssi: number): string => { @@ -176,6 +447,129 @@ export default function SetupWiFiScreen() { return 'wifi-outline'; }; + // Results screen + if (phase === 'results') { + const successSensors = sensors.filter(s => s.status === 'success'); + const failedSensors = sensors.filter(s => s.status === 'error' || s.status === 'skipped'); + + return ( + + + + Setup Complete + + + + + {/* Success Summary */} + + + 0 ? 'checkmark-circle' : 'alert-circle'} + size={48} + color={successSensors.length > 0 ? AppColors.success : AppColors.warning} + /> + + + {successSensors.length === sensors.length + ? 'All Sensors Connected!' + : successSensors.length > 0 + ? 'Partial Success' + : 'Setup Failed'} + + + {successSensors.length} of {sensors.length} sensors configured + + + + {/* Success List */} + {successSensors.length > 0 && ( + + Successfully Connected + {successSensors.map(sensor => ( + + + {sensor.deviceName} + + ))} + + )} + + {/* Failed List */} + {failedSensors.length > 0 && ( + + Failed + {failedSensors.map(sensor => ( + + + + {sensor.deviceName} + {sensor.error && ( + {sensor.error} + )} + {sensor.status === 'skipped' && ( + Skipped + )} + + + ))} + + )} + + {/* Info */} + + + + What's Next + + + {successSensors.length > 0 + ? '• Successfully connected sensors will appear in your Equipment list\n• It may take up to 1 minute for sensors to come online\n• You can configure sensor locations in Device Settings' + : '• Return to the Equipment screen and try adding sensors again\n• Make sure sensors are powered on and nearby'} + + + + + {/* Done Button */} + + + Done + + + + ); + } + + // Batch setup progress screen + if (phase === 'batch_setup') { + return ( + + + + Setting Up Sensors + + + + + + + + ); + } + + // WiFi selection screen (default) return ( {/* Header */} @@ -183,7 +577,6 @@ export default function SetupWiFiScreen() { { - // Disconnect all BLE devices before going back selectedDevices.forEach(d => disconnectDevice(d.id)); router.back(); }} @@ -203,8 +596,8 @@ export default function SetupWiFiScreen() { {selectedDevices.length === 1 ? ( <> - {deviceName} - Well ID: {wellId} + {firstDevice?.name} + Well ID: {firstDevice?.wellId} ) : ( <> @@ -318,22 +711,17 @@ export default function SetupWiFiScreen() { - {isConnecting ? ( - <> - - Connecting... - - ) : ( - <> - - Connect & Complete Setup - - )} + + + {selectedDevices.length === 1 + ? 'Connect & Complete Setup' + : `Connect All ${selectedDevices.length} Sensors`} + )} @@ -388,6 +776,10 @@ const styles = StyleSheet.create({ padding: Spacing.lg, paddingBottom: Spacing.xxl, }, + batchContent: { + flex: 1, + padding: Spacing.lg, + }, // Device Card deviceCard: { flexDirection: 'row', @@ -596,4 +988,79 @@ const styles = StyleSheet.create({ color: AppColors.info, lineHeight: 20, }, + // Results Screen + resultsSummary: { + alignItems: 'center', + paddingVertical: Spacing.xl, + }, + summaryIcon: { + width: 80, + height: 80, + borderRadius: 40, + justifyContent: 'center', + alignItems: 'center', + marginBottom: Spacing.md, + }, + summaryTitle: { + fontSize: FontSizes.xl, + fontWeight: FontWeights.bold, + color: AppColors.textPrimary, + marginBottom: Spacing.xs, + }, + summarySubtitle: { + fontSize: FontSizes.base, + color: AppColors.textSecondary, + }, + resultsSection: { + backgroundColor: AppColors.surface, + borderRadius: BorderRadius.lg, + padding: Spacing.md, + marginBottom: Spacing.md, + ...Shadows.xs, + }, + resultsSectionTitle: { + fontSize: FontSizes.sm, + fontWeight: FontWeights.semibold, + color: AppColors.textSecondary, + marginBottom: Spacing.sm, + textTransform: 'uppercase', + letterSpacing: 0.5, + }, + resultItem: { + flexDirection: 'row', + alignItems: 'flex-start', + paddingVertical: Spacing.xs, + gap: Spacing.sm, + }, + resultItemContent: { + flex: 1, + }, + resultItemText: { + fontSize: FontSizes.base, + fontWeight: FontWeights.medium, + color: AppColors.textPrimary, + }, + resultItemError: { + fontSize: FontSizes.xs, + color: AppColors.error, + marginTop: 2, + }, + bottomActions: { + padding: Spacing.lg, + borderTopWidth: 1, + borderTopColor: AppColors.border, + backgroundColor: AppColors.surface, + }, + doneButton: { + backgroundColor: AppColors.primary, + paddingVertical: Spacing.md, + borderRadius: BorderRadius.lg, + alignItems: 'center', + ...Shadows.md, + }, + doneButtonText: { + fontSize: FontSizes.base, + fontWeight: FontWeights.semibold, + color: AppColors.white, + }, }); diff --git a/components/BatchSetupProgress.tsx b/components/BatchSetupProgress.tsx new file mode 100644 index 0000000..cb0e5eb --- /dev/null +++ b/components/BatchSetupProgress.tsx @@ -0,0 +1,394 @@ +import React from 'react'; +import { + View, + Text, + StyleSheet, + ScrollView, + TouchableOpacity, + ActivityIndicator, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import type { SensorSetupState, SensorSetupStep } from '@/types'; +import { + AppColors, + BorderRadius, + FontSizes, + FontWeights, + Spacing, + Shadows, +} from '@/constants/theme'; + +interface BatchSetupProgressProps { + sensors: SensorSetupState[]; + currentIndex: number; + ssid: string; + isPaused: boolean; + onRetry?: (deviceId: string) => void; + onSkip?: (deviceId: string) => void; + onCancelAll?: () => void; +} + +const STEP_LABELS: Record = { + connect: 'Connecting', + unlock: 'Unlocking', + wifi: 'Setting WiFi', + attach: 'Registering', + reboot: 'Rebooting', +}; + +function StepIndicator({ step }: { step: SensorSetupStep }) { + const getIcon = () => { + switch (step.status) { + case 'completed': + return ; + case 'in_progress': + return ; + case 'failed': + return ; + default: + return ; + } + }; + + const getTextColor = () => { + switch (step.status) { + case 'completed': + return AppColors.success; + case 'in_progress': + return AppColors.primary; + case 'failed': + return AppColors.error; + default: + return AppColors.textMuted; + } + }; + + return ( + + {getIcon()} + + {STEP_LABELS[step.name]} + + {step.error && ( + {step.error} + )} + + ); +} + +function SensorCard({ + sensor, + isActive, + onRetry, + onSkip, +}: { + sensor: SensorSetupState; + isActive: boolean; + onRetry?: () => void; + onSkip?: () => void; +}) { + const getStatusColor = () => { + switch (sensor.status) { + case 'success': + return AppColors.success; + case 'error': + return AppColors.error; + case 'skipped': + return AppColors.warning; + case 'pending': + return AppColors.textMuted; + default: + return AppColors.primary; + } + }; + + const getStatusIcon = () => { + switch (sensor.status) { + case 'success': + return ; + case 'error': + return ; + case 'skipped': + return ; + case 'pending': + return ; + default: + return ; + } + }; + + const showActions = sensor.status === 'error' && onRetry && onSkip; + + return ( + + + + + + + {sensor.deviceName} + {sensor.wellId && ( + Well ID: {sensor.wellId} + )} + + {getStatusIcon()} + + + {/* Show steps for active or completed sensors */} + {(isActive || sensor.status === 'success' || sensor.status === 'error') && ( + + {sensor.steps.map((step, index) => ( + + ))} + + )} + + {/* Error message */} + {sensor.error && ( + + + {sensor.error} + + )} + + {/* Action buttons for failed sensors */} + {showActions && ( + + + + Retry + + + + Skip + + + )} + + ); +} + +export default function BatchSetupProgress({ + sensors, + currentIndex, + ssid, + isPaused, + onRetry, + onSkip, + onCancelAll, +}: BatchSetupProgressProps) { + const completedCount = sensors.filter(s => s.status === 'success').length; + const failedCount = sensors.filter(s => s.status === 'error').length; + const skippedCount = sensors.filter(s => s.status === 'skipped').length; + const totalProcessed = completedCount + failedCount + skippedCount; + const progress = (totalProcessed / sensors.length) * 100; + + return ( + + {/* Progress Header */} + + + Connecting sensors to "{ssid}"... + + + {totalProcessed} of {sensors.length} complete + + + {/* Progress bar */} + + + + + + {/* Sensors List */} + + {sensors.map((sensor, index) => ( + onRetry(sensor.deviceId) : undefined} + onSkip={onSkip ? () => onSkip(sensor.deviceId) : undefined} + /> + ))} + + + {/* Cancel button */} + {onCancelAll && ( + + Cancel Setup + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + progressHeader: { + marginBottom: Spacing.lg, + }, + progressTitle: { + fontSize: FontSizes.lg, + fontWeight: FontWeights.semibold, + color: AppColors.textPrimary, + marginBottom: Spacing.xs, + }, + progressSubtitle: { + fontSize: FontSizes.sm, + color: AppColors.textSecondary, + marginBottom: Spacing.md, + }, + progressBarContainer: { + height: 4, + backgroundColor: AppColors.border, + borderRadius: 2, + overflow: 'hidden', + }, + progressBar: { + height: '100%', + backgroundColor: AppColors.primary, + borderRadius: 2, + }, + sensorsList: { + flex: 1, + }, + sensorsListContent: { + gap: Spacing.md, + paddingBottom: Spacing.lg, + }, + sensorCard: { + backgroundColor: AppColors.surface, + borderRadius: BorderRadius.lg, + padding: Spacing.md, + ...Shadows.xs, + }, + sensorCardActive: { + borderWidth: 2, + borderColor: AppColors.primary, + }, + sensorHeader: { + flexDirection: 'row', + alignItems: 'center', + }, + sensorIcon: { + width: 40, + height: 40, + borderRadius: BorderRadius.md, + backgroundColor: AppColors.primaryLighter, + justifyContent: 'center', + alignItems: 'center', + marginRight: Spacing.sm, + }, + sensorInfo: { + flex: 1, + }, + sensorName: { + fontSize: FontSizes.base, + fontWeight: FontWeights.semibold, + color: AppColors.textPrimary, + }, + sensorMeta: { + fontSize: FontSizes.xs, + color: AppColors.textMuted, + }, + statusIcon: { + marginLeft: Spacing.sm, + }, + stepsContainer: { + marginTop: Spacing.md, + paddingTop: Spacing.sm, + borderTopWidth: 1, + borderTopColor: AppColors.border, + gap: Spacing.xs, + }, + stepRow: { + flexDirection: 'row', + alignItems: 'center', + gap: Spacing.sm, + }, + stepIcon: { + width: 16, + height: 16, + justifyContent: 'center', + alignItems: 'center', + }, + pendingDot: { + width: 6, + height: 6, + borderRadius: 3, + backgroundColor: AppColors.textMuted, + }, + stepLabel: { + fontSize: FontSizes.sm, + fontWeight: FontWeights.medium, + }, + stepError: { + fontSize: FontSizes.xs, + color: AppColors.error, + flex: 1, + }, + errorContainer: { + flexDirection: 'row', + alignItems: 'center', + marginTop: Spacing.sm, + padding: Spacing.sm, + backgroundColor: AppColors.errorLight, + borderRadius: BorderRadius.sm, + gap: Spacing.xs, + }, + errorText: { + fontSize: FontSizes.xs, + color: AppColors.error, + flex: 1, + }, + actionButtons: { + flexDirection: 'row', + marginTop: Spacing.md, + gap: Spacing.md, + }, + retryButton: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: Spacing.xs, + paddingHorizontal: Spacing.md, + backgroundColor: AppColors.primaryLighter, + borderRadius: BorderRadius.md, + gap: Spacing.xs, + }, + retryText: { + fontSize: FontSizes.sm, + fontWeight: FontWeights.medium, + color: AppColors.primary, + }, + skipButton: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: Spacing.xs, + paddingHorizontal: Spacing.md, + backgroundColor: AppColors.surfaceSecondary, + borderRadius: BorderRadius.md, + gap: Spacing.xs, + }, + skipText: { + fontSize: FontSizes.sm, + fontWeight: FontWeights.medium, + color: AppColors.textMuted, + }, + cancelAllButton: { + alignItems: 'center', + paddingVertical: Spacing.sm, + marginTop: Spacing.md, + }, + cancelAllText: { + fontSize: FontSizes.sm, + fontWeight: FontWeights.medium, + color: AppColors.error, + }, +}); diff --git a/types/index.ts b/types/index.ts index 03dd7b5..c2190b5 100644 --- a/types/index.ts +++ b/types/index.ts @@ -67,6 +67,18 @@ export type EquipmentStatus = | 'active' // Equipment activated and working | 'demo'; // Demo mode (DEMO-00000) +// Deployment (location where beneficiary can be monitored) +export interface Deployment { + id: number; + beneficiary_id: number; + name: string; // e.g., "Home", "Office", "Vacation Home" + address?: string; + is_primary: boolean; // One deployment per beneficiary is primary + legacy_deployment_id?: number; // Link to Legacy API deployment + created_at: string; + updated_at: string; +} + // Beneficiary Types (elderly people being monitored) export interface Beneficiary { id: number; @@ -193,3 +205,48 @@ export interface ApiResponse { error?: ApiError; ok: boolean; } + +// Batch Sensor Setup Types + +/** States a sensor can be in during batch setup */ +export type SensorSetupStatus = + | 'pending' // Waiting in queue + | 'connecting' // BLE connection in progress + | 'unlocking' // Sending PIN command + | 'setting_wifi' // Configuring WiFi + | 'attaching' // Calling Legacy API to link to deployment + | 'rebooting' // Restarting sensor + | 'success' // Completed successfully + | 'error' // Failed (with error message) + | 'skipped'; // User chose to skip after error + +/** Step within a sensor's setup process */ +export interface SensorSetupStep { + name: 'connect' | 'unlock' | 'wifi' | 'attach' | 'reboot'; + status: 'pending' | 'in_progress' | 'completed' | 'failed'; + error?: string; +} + +/** State of a single sensor during batch setup */ +export interface SensorSetupState { + deviceId: string; + deviceName: string; + wellId?: number; + mac: string; + status: SensorSetupStatus; + steps: SensorSetupStep[]; + error?: string; + startTime?: number; + endTime?: number; +} + +/** Overall batch setup state */ +export interface BatchSetupState { + sensors: SensorSetupState[]; + currentIndex: number; + ssid: string; + password: string; + isPaused: boolean; + isComplete: boolean; + startTime: number; +}