WellNuo/components/BatchSetupProgress.tsx
Sergei be1c2eb7f5 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>
2026-01-19 22:47:48 -08:00

395 lines
10 KiB
TypeScript

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,
},
});