import React, { useRef, useEffect, useState } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Animated, DimensionValue, } 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; } // Format elapsed time as "Xs" or "Xm Xs" function formatElapsedTime(startTime?: number, endTime?: number): string { if (!startTime) return ''; const end = endTime || Date.now(); const elapsed = Math.floor((end - startTime) / 1000); if (elapsed < 60) return `${elapsed}s`; const minutes = Math.floor(elapsed / 60); const seconds = elapsed % 60; return `${minutes}m ${seconds}s`; } 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, index, total, onRetry, onSkip, }: { sensor: SensorSetupState; isActive: boolean; index: number; total: number; onRetry?: () => void; onSkip?: () => void; }) { const [elapsedTime, setElapsedTime] = useState(''); // Update elapsed time for active sensors useEffect(() => { if (isActive && sensor.startTime && !sensor.endTime) { const interval = setInterval(() => { setElapsedTime(formatElapsedTime(sensor.startTime)); }, 1000); return () => clearInterval(interval); } else if (sensor.endTime && sensor.startTime) { setElapsedTime(formatElapsedTime(sensor.startTime, sensor.endTime)); } }, [isActive, sensor.startTime, sensor.endTime]); 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; const showProgress = sensor.status !== 'pending' && sensor.status !== 'skipped'; return ( {/* Index Badge */} {index + 1}/{total} {sensor.deviceName} {sensor.wellId && ( Well ID: {sensor.wellId} )} {elapsedTime && showProgress && ( {elapsedTime} )} {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 scrollViewRef = useRef(null); const sensorCardRefs = useRef<{ [key: string]: View | null }>({}); const progressAnim = useRef(new Animated.Value(0)).current; 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; // Animate progress bar useEffect(() => { Animated.timing(progressAnim, { toValue: progress, duration: 300, useNativeDriver: false, }).start(); }, [progress, progressAnim]); // Auto-scroll to current sensor useEffect(() => { if (currentIndex >= 0 && scrollViewRef.current) { // Calculate approximate scroll position (each card ~120px + spacing) const cardHeight = 120; const spacing = 12; const scrollTo = currentIndex * (cardHeight + spacing); setTimeout(() => { scrollViewRef.current?.scrollTo({ y: Math.max(0, scrollTo - 20), // Small offset for visibility animated: true, }); }, 100); } }, [currentIndex]); const animatedWidth = progressAnim.interpolate({ inputRange: [0, 100], outputRange: ['0%', '100%'], }); return ( {/* Progress Header */} Connecting to "{ssid}" {!isPaused && currentIndex < sensors.length && ( {currentIndex + 1}/{sensors.length} )} {isPaused ? ( Paused - action required ) : totalProcessed === sensors.length ? ( 'Setup complete!' ) : ( `Processing sensor ${currentIndex + 1} of ${sensors.length}...` )} {/* Animated Progress bar */} {/* Success/Error segments */} {sensors.map((sensor) => { const segmentWidth: DimensionValue = `${100 / sensors.length}%`; let backgroundColor = 'transparent'; if (sensor.status === 'success') backgroundColor = AppColors.success; else if (sensor.status === 'error') backgroundColor = AppColors.error; else if (sensor.status === 'skipped') backgroundColor = AppColors.warning; return ( ); })} {/* Stats Row */} {completedCount > 0 && ( {completedCount} )} {failedCount > 0 && ( {failedCount} )} {skippedCount > 0 && ( {skippedCount} )} {/* 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, }, progressTitleRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: Spacing.xs, }, progressTitle: { fontSize: FontSizes.lg, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, flex: 1, }, currentSensorBadge: { backgroundColor: AppColors.primary, paddingHorizontal: Spacing.sm, paddingVertical: Spacing.xs, borderRadius: BorderRadius.md, }, currentSensorBadgeText: { fontSize: FontSizes.xs, fontWeight: FontWeights.bold, color: AppColors.white, }, progressSubtitle: { fontSize: FontSizes.sm, color: AppColors.textSecondary, marginBottom: Spacing.md, }, pausedText: { color: AppColors.warning, fontWeight: FontWeights.medium, }, progressBarContainer: { height: 6, backgroundColor: AppColors.border, borderRadius: 3, overflow: 'hidden', position: 'relative', }, progressBar: { position: 'absolute', top: 0, left: 0, height: '100%', backgroundColor: AppColors.primary, borderRadius: 3, opacity: 0.3, }, progressSegments: { flexDirection: 'row', height: '100%', position: 'absolute', top: 0, left: 0, right: 0, }, progressSegment: { height: '100%', }, statsRow: { flexDirection: 'row', marginTop: Spacing.sm, gap: Spacing.md, }, statItem: { flexDirection: 'row', alignItems: 'center', gap: 4, }, statText: { fontSize: FontSizes.xs, fontWeight: FontWeights.medium, }, sensorsList: { flex: 1, }, sensorsListContent: { gap: Spacing.md, paddingBottom: Spacing.lg, }, sensorCard: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, padding: Spacing.md, position: 'relative', overflow: 'hidden', ...Shadows.xs, }, sensorCardActive: { borderWidth: 2, borderColor: AppColors.primary, }, sensorCardSuccess: { borderWidth: 1, borderColor: AppColors.success, }, sensorCardError: { borderWidth: 1, borderColor: AppColors.error, }, indexBadge: { position: 'absolute', top: 0, right: 0, paddingHorizontal: Spacing.sm, paddingVertical: 2, borderBottomLeftRadius: BorderRadius.md, }, indexBadgeText: { fontSize: FontSizes.xs, fontWeight: FontWeights.bold, color: AppColors.white, }, 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, }, sensorMetaRow: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, }, sensorMeta: { fontSize: FontSizes.xs, color: AppColors.textMuted, }, elapsedTime: { fontSize: FontSizes.xs, fontWeight: FontWeights.medium, color: AppColors.primary, }, 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, }, });