Refactor Setup WiFi screen for batch sensor processing
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
b738d86419
commit
be1c2eb7f5
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@ -16,6 +16,12 @@ import * as Device from 'expo-device';
|
|||||||
import { useBLE } from '@/contexts/BLEContext';
|
import { useBLE } from '@/contexts/BLEContext';
|
||||||
import { api } from '@/services/api';
|
import { api } from '@/services/api';
|
||||||
import type { WiFiNetwork } from '@/services/ble';
|
import type { WiFiNetwork } from '@/services/ble';
|
||||||
|
import type {
|
||||||
|
SensorSetupState,
|
||||||
|
SensorSetupStep,
|
||||||
|
SensorSetupStatus,
|
||||||
|
} from '@/types';
|
||||||
|
import BatchSetupProgress from '@/components/BatchSetupProgress';
|
||||||
import {
|
import {
|
||||||
AppColors,
|
AppColors,
|
||||||
BorderRadius,
|
BorderRadius,
|
||||||
@ -33,13 +39,44 @@ interface DeviceParam {
|
|||||||
wellId?: number;
|
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() {
|
export default function SetupWiFiScreen() {
|
||||||
const { id, devices: devicesParam } = useLocalSearchParams<{
|
const { id, devices: devicesParam } = useLocalSearchParams<{
|
||||||
id: string;
|
id: string;
|
||||||
devices: string; // JSON string of DeviceParam[]
|
devices: string; // JSON string of DeviceParam[]
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { getWiFiList, setWiFi, disconnectDevice } = useBLE();
|
const {
|
||||||
|
getWiFiList,
|
||||||
|
setWiFi,
|
||||||
|
connectDevice,
|
||||||
|
disconnectDevice,
|
||||||
|
rebootDevice,
|
||||||
|
} = useBLE();
|
||||||
|
|
||||||
// Parse devices from navigation params
|
// Parse devices from navigation params
|
||||||
const selectedDevices: DeviceParam[] = React.useMemo(() => {
|
const selectedDevices: DeviceParam[] = React.useMemo(() => {
|
||||||
@ -52,28 +89,37 @@ export default function SetupWiFiScreen() {
|
|||||||
}
|
}
|
||||||
}, [devicesParam]);
|
}, [devicesParam]);
|
||||||
|
|
||||||
// Use first device for WiFi scanning (all devices will use same WiFi)
|
// Use first device for WiFi scanning
|
||||||
const firstDevice = selectedDevices[0];
|
const firstDevice = selectedDevices[0];
|
||||||
const deviceId = firstDevice?.id;
|
const deviceId = firstDevice?.id;
|
||||||
const deviceName = firstDevice?.name;
|
|
||||||
const wellId = firstDevice?.wellId?.toString();
|
|
||||||
|
|
||||||
|
// UI Phase
|
||||||
|
const [phase, setPhase] = useState<SetupPhase>('wifi_selection');
|
||||||
|
|
||||||
|
// WiFi selection state
|
||||||
const [networks, setNetworks] = useState<WiFiNetwork[]>([]);
|
const [networks, setNetworks] = useState<WiFiNetwork[]>([]);
|
||||||
const [isLoadingNetworks, setIsLoadingNetworks] = useState(false);
|
const [isLoadingNetworks, setIsLoadingNetworks] = useState(false);
|
||||||
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork | null>(null);
|
const [selectedNetwork, setSelectedNetwork] = useState<WiFiNetwork | null>(null);
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [isConnecting, setIsConnecting] = useState(false);
|
|
||||||
|
// Batch setup state
|
||||||
|
const [sensors, setSensors] = useState<SensorSetupState[]>([]);
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [isPaused, setIsPaused] = useState(false);
|
||||||
|
const setupInProgressRef = useRef(false);
|
||||||
|
const shouldCancelRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadWiFiNetworks();
|
loadWiFiNetworks();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadWiFiNetworks = async () => {
|
const loadWiFiNetworks = async () => {
|
||||||
|
if (!deviceId) return;
|
||||||
setIsLoadingNetworks(true);
|
setIsLoadingNetworks(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const wifiList = await getWiFiList(deviceId!);
|
const wifiList = await getWiFiList(deviceId);
|
||||||
setNetworks(wifiList);
|
setNetworks(wifiList);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('[SetupWiFi] Failed to get WiFi list:', error);
|
console.error('[SetupWiFi] Failed to get WiFi list:', error);
|
||||||
@ -88,71 +134,296 @@ export default function SetupWiFiScreen() {
|
|||||||
setPassword('');
|
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<boolean> => {
|
||||||
|
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) {
|
if (!selectedNetwork) {
|
||||||
Alert.alert('Error', 'Please select a WiFi network');
|
Alert.alert('Error', 'Please select a WiFi network');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password) {
|
if (!password) {
|
||||||
Alert.alert('Error', 'Please enter WiFi password');
|
Alert.alert('Error', 'Please enter WiFi password');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsConnecting(true);
|
// Initialize sensor states
|
||||||
|
const initialStates = selectedDevices.map(createSensorState);
|
||||||
|
setSensors(initialStates);
|
||||||
|
setCurrentIndex(0);
|
||||||
|
setIsPaused(false);
|
||||||
|
setPhase('batch_setup');
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
// Start processing after phase change
|
||||||
// Step 1: Set WiFi on the device via BLE
|
useEffect(() => {
|
||||||
const success = await setWiFi(deviceId!, selectedNetwork.ssid, password);
|
if (phase === 'batch_setup' && sensors.length > 0 && !setupInProgressRef.current) {
|
||||||
|
runBatchSetup();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
}, [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 => {
|
const getSignalStrength = (rssi: number): string => {
|
||||||
@ -176,6 +447,129 @@ export default function SetupWiFiScreen() {
|
|||||||
return 'wifi-outline';
|
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 (
|
||||||
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View style={styles.placeholder} />
|
||||||
|
<Text style={styles.headerTitle}>Setup Complete</Text>
|
||||||
|
<View style={styles.placeholder} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.content} contentContainerStyle={styles.scrollContent}>
|
||||||
|
{/* Success Summary */}
|
||||||
|
<View style={styles.resultsSummary}>
|
||||||
|
<View style={[styles.summaryIcon, { backgroundColor: AppColors.successLight }]}>
|
||||||
|
<Ionicons
|
||||||
|
name={successSensors.length > 0 ? 'checkmark-circle' : 'alert-circle'}
|
||||||
|
size={48}
|
||||||
|
color={successSensors.length > 0 ? AppColors.success : AppColors.warning}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.summaryTitle}>
|
||||||
|
{successSensors.length === sensors.length
|
||||||
|
? 'All Sensors Connected!'
|
||||||
|
: successSensors.length > 0
|
||||||
|
? 'Partial Success'
|
||||||
|
: 'Setup Failed'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.summarySubtitle}>
|
||||||
|
{successSensors.length} of {sensors.length} sensors configured
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Success List */}
|
||||||
|
{successSensors.length > 0 && (
|
||||||
|
<View style={styles.resultsSection}>
|
||||||
|
<Text style={styles.resultsSectionTitle}>Successfully Connected</Text>
|
||||||
|
{successSensors.map(sensor => (
|
||||||
|
<View key={sensor.deviceId} style={styles.resultItem}>
|
||||||
|
<Ionicons name="checkmark-circle" size={20} color={AppColors.success} />
|
||||||
|
<Text style={styles.resultItemText}>{sensor.deviceName}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Failed List */}
|
||||||
|
{failedSensors.length > 0 && (
|
||||||
|
<View style={styles.resultsSection}>
|
||||||
|
<Text style={styles.resultsSectionTitle}>Failed</Text>
|
||||||
|
{failedSensors.map(sensor => (
|
||||||
|
<View key={sensor.deviceId} style={styles.resultItem}>
|
||||||
|
<Ionicons
|
||||||
|
name={sensor.status === 'skipped' ? 'remove-circle' : 'close-circle'}
|
||||||
|
size={20}
|
||||||
|
color={sensor.status === 'skipped' ? AppColors.warning : AppColors.error}
|
||||||
|
/>
|
||||||
|
<View style={styles.resultItemContent}>
|
||||||
|
<Text style={styles.resultItemText}>{sensor.deviceName}</Text>
|
||||||
|
{sensor.error && (
|
||||||
|
<Text style={styles.resultItemError}>{sensor.error}</Text>
|
||||||
|
)}
|
||||||
|
{sensor.status === 'skipped' && (
|
||||||
|
<Text style={styles.resultItemError}>Skipped</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Info */}
|
||||||
|
<View style={styles.helpCard}>
|
||||||
|
<View style={styles.helpHeader}>
|
||||||
|
<Ionicons name="information-circle" size={20} color={AppColors.info} />
|
||||||
|
<Text style={styles.helpTitle}>What's Next</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.helpText}>
|
||||||
|
{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'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Done Button */}
|
||||||
|
<View style={styles.bottomActions}>
|
||||||
|
<TouchableOpacity style={styles.doneButton} onPress={handleDone}>
|
||||||
|
<Text style={styles.doneButtonText}>Done</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch setup progress screen
|
||||||
|
if (phase === 'batch_setup') {
|
||||||
|
return (
|
||||||
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View style={styles.placeholder} />
|
||||||
|
<Text style={styles.headerTitle}>Setting Up Sensors</Text>
|
||||||
|
<View style={styles.placeholder} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.batchContent}>
|
||||||
|
<BatchSetupProgress
|
||||||
|
sensors={sensors}
|
||||||
|
currentIndex={currentIndex}
|
||||||
|
ssid={selectedNetwork?.ssid || ''}
|
||||||
|
isPaused={isPaused}
|
||||||
|
onRetry={handleRetry}
|
||||||
|
onSkip={handleSkip}
|
||||||
|
onCancelAll={handleCancelAll}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WiFi selection screen (default)
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
<SafeAreaView style={styles.container} edges={['top', 'bottom']}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -183,7 +577,6 @@ export default function SetupWiFiScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
// Disconnect all BLE devices before going back
|
|
||||||
selectedDevices.forEach(d => disconnectDevice(d.id));
|
selectedDevices.forEach(d => disconnectDevice(d.id));
|
||||||
router.back();
|
router.back();
|
||||||
}}
|
}}
|
||||||
@ -203,8 +596,8 @@ export default function SetupWiFiScreen() {
|
|||||||
<View style={styles.deviceInfo}>
|
<View style={styles.deviceInfo}>
|
||||||
{selectedDevices.length === 1 ? (
|
{selectedDevices.length === 1 ? (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.deviceName}>{deviceName}</Text>
|
<Text style={styles.deviceName}>{firstDevice?.name}</Text>
|
||||||
<Text style={styles.deviceMeta}>Well ID: {wellId}</Text>
|
<Text style={styles.deviceMeta}>Well ID: {firstDevice?.wellId}</Text>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -318,22 +711,17 @@ export default function SetupWiFiScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
style={[
|
||||||
styles.connectButton,
|
styles.connectButton,
|
||||||
(!password || isConnecting) && styles.connectButtonDisabled,
|
!password && styles.connectButtonDisabled,
|
||||||
]}
|
]}
|
||||||
onPress={handleConnect}
|
onPress={handleStartBatchSetup}
|
||||||
disabled={!password || isConnecting}
|
disabled={!password}
|
||||||
>
|
>
|
||||||
{isConnecting ? (
|
<Ionicons name="checkmark" size={20} color={AppColors.white} />
|
||||||
<>
|
<Text style={styles.connectButtonText}>
|
||||||
<ActivityIndicator size="small" color={AppColors.white} />
|
{selectedDevices.length === 1
|
||||||
<Text style={styles.connectButtonText}>Connecting...</Text>
|
? 'Connect & Complete Setup'
|
||||||
</>
|
: `Connect All ${selectedDevices.length} Sensors`}
|
||||||
) : (
|
</Text>
|
||||||
<>
|
|
||||||
<Ionicons name="checkmark" size={20} color={AppColors.white} />
|
|
||||||
<Text style={styles.connectButtonText}>Connect & Complete Setup</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
@ -388,6 +776,10 @@ const styles = StyleSheet.create({
|
|||||||
padding: Spacing.lg,
|
padding: Spacing.lg,
|
||||||
paddingBottom: Spacing.xxl,
|
paddingBottom: Spacing.xxl,
|
||||||
},
|
},
|
||||||
|
batchContent: {
|
||||||
|
flex: 1,
|
||||||
|
padding: Spacing.lg,
|
||||||
|
},
|
||||||
// Device Card
|
// Device Card
|
||||||
deviceCard: {
|
deviceCard: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@ -596,4 +988,79 @@ const styles = StyleSheet.create({
|
|||||||
color: AppColors.info,
|
color: AppColors.info,
|
||||||
lineHeight: 20,
|
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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
394
components/BatchSetupProgress.tsx
Normal file
394
components/BatchSetupProgress.tsx
Normal file
@ -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<SensorSetupStep['name'], string> = {
|
||||||
|
connect: 'Connecting',
|
||||||
|
unlock: 'Unlocking',
|
||||||
|
wifi: 'Setting WiFi',
|
||||||
|
attach: 'Registering',
|
||||||
|
reboot: 'Rebooting',
|
||||||
|
};
|
||||||
|
|
||||||
|
function StepIndicator({ step }: { step: SensorSetupStep }) {
|
||||||
|
const getIcon = () => {
|
||||||
|
switch (step.status) {
|
||||||
|
case 'completed':
|
||||||
|
return <Ionicons name="checkmark" size={12} color={AppColors.success} />;
|
||||||
|
case 'in_progress':
|
||||||
|
return <ActivityIndicator size={10} color={AppColors.primary} />;
|
||||||
|
case 'failed':
|
||||||
|
return <Ionicons name="close" size={12} color={AppColors.error} />;
|
||||||
|
default:
|
||||||
|
return <View style={styles.pendingDot} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View style={styles.stepRow}>
|
||||||
|
<View style={styles.stepIcon}>{getIcon()}</View>
|
||||||
|
<Text style={[styles.stepLabel, { color: getTextColor() }]}>
|
||||||
|
{STEP_LABELS[step.name]}
|
||||||
|
</Text>
|
||||||
|
{step.error && (
|
||||||
|
<Text style={styles.stepError}>{step.error}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <Ionicons name="checkmark-circle" size={24} color={AppColors.success} />;
|
||||||
|
case 'error':
|
||||||
|
return <Ionicons name="close-circle" size={24} color={AppColors.error} />;
|
||||||
|
case 'skipped':
|
||||||
|
return <Ionicons name="remove-circle" size={24} color={AppColors.warning} />;
|
||||||
|
case 'pending':
|
||||||
|
return <Ionicons name="ellipse-outline" size={24} color={AppColors.textMuted} />;
|
||||||
|
default:
|
||||||
|
return <ActivityIndicator size={20} color={AppColors.primary} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showActions = sensor.status === 'error' && onRetry && onSkip;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.sensorCard, isActive && styles.sensorCardActive]}>
|
||||||
|
<View style={styles.sensorHeader}>
|
||||||
|
<View style={styles.sensorIcon}>
|
||||||
|
<Ionicons name="water" size={20} color={getStatusColor()} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.sensorInfo}>
|
||||||
|
<Text style={styles.sensorName}>{sensor.deviceName}</Text>
|
||||||
|
{sensor.wellId && (
|
||||||
|
<Text style={styles.sensorMeta}>Well ID: {sensor.wellId}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<View style={styles.statusIcon}>{getStatusIcon()}</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Show steps for active or completed sensors */}
|
||||||
|
{(isActive || sensor.status === 'success' || sensor.status === 'error') && (
|
||||||
|
<View style={styles.stepsContainer}>
|
||||||
|
{sensor.steps.map((step, index) => (
|
||||||
|
<StepIndicator key={step.name} step={step} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Error message */}
|
||||||
|
{sensor.error && (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<Ionicons name="alert-circle" size={16} color={AppColors.error} />
|
||||||
|
<Text style={styles.errorText}>{sensor.error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Action buttons for failed sensors */}
|
||||||
|
{showActions && (
|
||||||
|
<View style={styles.actionButtons}>
|
||||||
|
<TouchableOpacity style={styles.retryButton} onPress={onRetry}>
|
||||||
|
<Ionicons name="refresh" size={16} color={AppColors.primary} />
|
||||||
|
<Text style={styles.retryText}>Retry</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={styles.skipButton} onPress={onSkip}>
|
||||||
|
<Ionicons name="arrow-forward" size={16} color={AppColors.textMuted} />
|
||||||
|
<Text style={styles.skipText}>Skip</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{/* Progress Header */}
|
||||||
|
<View style={styles.progressHeader}>
|
||||||
|
<Text style={styles.progressTitle}>
|
||||||
|
Connecting sensors to "{ssid}"...
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.progressSubtitle}>
|
||||||
|
{totalProcessed} of {sensors.length} complete
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Progress bar */}
|
||||||
|
<View style={styles.progressBarContainer}>
|
||||||
|
<View style={[styles.progressBar, { width: `${progress}%` }]} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Sensors List */}
|
||||||
|
<ScrollView
|
||||||
|
style={styles.sensorsList}
|
||||||
|
contentContainerStyle={styles.sensorsListContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
{sensors.map((sensor, index) => (
|
||||||
|
<SensorCard
|
||||||
|
key={sensor.deviceId}
|
||||||
|
sensor={sensor}
|
||||||
|
isActive={index === currentIndex && !isPaused}
|
||||||
|
onRetry={onRetry ? () => onRetry(sensor.deviceId) : undefined}
|
||||||
|
onSkip={onSkip ? () => onSkip(sensor.deviceId) : undefined}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Cancel button */}
|
||||||
|
{onCancelAll && (
|
||||||
|
<TouchableOpacity style={styles.cancelAllButton} onPress={onCancelAll}>
|
||||||
|
<Text style={styles.cancelAllText}>Cancel Setup</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -67,6 +67,18 @@ export type EquipmentStatus =
|
|||||||
| 'active' // Equipment activated and working
|
| 'active' // Equipment activated and working
|
||||||
| 'demo'; // Demo mode (DEMO-00000)
|
| '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)
|
// Beneficiary Types (elderly people being monitored)
|
||||||
export interface Beneficiary {
|
export interface Beneficiary {
|
||||||
id: number;
|
id: number;
|
||||||
@ -193,3 +205,48 @@ export interface ApiResponse<T> {
|
|||||||
error?: ApiError;
|
error?: ApiError;
|
||||||
ok: boolean;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user