import React, { useRef, useEffect, useState } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Animated, DimensionValue, Modal, } 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'; // User-friendly error messages based on error type function getErrorMessage(error: string | undefined): { title: string; description: string; hint: string } { if (!error) { return { title: 'Unknown Error', description: 'Something went wrong.', hint: 'Try again or skip this sensor.', }; } const lowerError = error.toLowerCase(); if (lowerError.includes('connect') || lowerError.includes('connection')) { return { title: 'Connection Failed', description: 'Could not connect to the sensor via Bluetooth.', hint: 'Move closer to the sensor and ensure it\'s powered on.', }; } if (lowerError.includes('unlock') || lowerError.includes('pin')) { return { title: 'Unlock Failed', description: 'Could not unlock the sensor for configuration.', hint: 'The sensor may need to be reset. Try again.', }; } if (lowerError.includes('wifi') || lowerError.includes('network')) { return { title: 'WiFi Configuration Failed', description: 'Could not set up the WiFi connection on the sensor.', hint: 'Check that the WiFi password is correct.', }; } if (lowerError.includes('register') || lowerError.includes('attach') || lowerError.includes('api')) { return { title: 'Registration Failed', description: 'Could not register the sensor with your account.', hint: 'Check your internet connection and try again.', }; } if (lowerError.includes('timeout') || lowerError.includes('respond')) { return { title: 'Sensor Not Responding', description: 'The sensor stopped responding during setup.', hint: 'Move closer or check if the sensor is still powered on.', }; } if (lowerError.includes('reboot')) { return { title: 'Reboot Failed', description: 'Could not restart the sensor.', hint: 'The sensor may still work. Try checking it in Equipment.', }; } // Show the actual error message for debugging return { title: 'Setup Failed', description: error, hint: `Technical details: ${error}`, }; } interface BatchSetupProgressProps { sensors: SensorSetupState[]; currentIndex: number; ssid: string; isPaused: boolean; onRetry?: (deviceId: string) => void; onSkip?: (deviceId: string) => void; onCancelAll?: () => void; } // Error Action Modal Component function ErrorActionModal({ visible, sensor, onRetry, onSkip, onCancelAll, }: { visible: boolean; sensor: SensorSetupState | null; onRetry: () => void; onSkip: () => void; onCancelAll: () => void; }) { if (!sensor) return null; const errorInfo = getErrorMessage(sensor.error); return ( {/* Error Icon */} {/* Error Title */} {errorInfo.title} {/* Sensor Name */} {sensor.deviceName} {/* Error Description */} {errorInfo.description} {/* Hint */} {errorInfo.hint} {/* Action Buttons */} Retry Skip Sensor {/* Cancel All */} Cancel All Setup ); } const modalStyles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center', padding: Spacing.lg, }, container: { backgroundColor: AppColors.surface, borderRadius: BorderRadius.xl, padding: Spacing.xl, width: '100%', maxWidth: 340, alignItems: 'center', ...Shadows.lg, }, iconContainer: { width: 80, height: 80, borderRadius: 40, backgroundColor: AppColors.errorLight, justifyContent: 'center', alignItems: 'center', marginBottom: Spacing.md, }, title: { fontSize: FontSizes.xl, fontWeight: FontWeights.bold, color: AppColors.textPrimary, textAlign: 'center', marginBottom: Spacing.sm, }, sensorBadge: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.primaryLighter, paddingHorizontal: Spacing.sm, paddingVertical: Spacing.xs, borderRadius: BorderRadius.md, gap: Spacing.xs, marginBottom: Spacing.md, }, sensorName: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.primary, }, description: { fontSize: FontSizes.base, color: AppColors.textSecondary, textAlign: 'center', marginBottom: Spacing.md, lineHeight: 22, }, hintContainer: { flexDirection: 'row', alignItems: 'flex-start', backgroundColor: AppColors.infoLight, padding: Spacing.md, borderRadius: BorderRadius.md, marginBottom: Spacing.lg, gap: Spacing.sm, }, hintText: { fontSize: FontSizes.sm, color: AppColors.info, flex: 1, lineHeight: 20, }, actions: { flexDirection: 'row', gap: Spacing.md, marginBottom: Spacing.md, }, retryButton: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: AppColors.primary, paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, gap: Spacing.xs, ...Shadows.sm, }, retryText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.white, }, skipButton: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: AppColors.surfaceSecondary, paddingVertical: Spacing.md, borderRadius: BorderRadius.lg, gap: Spacing.xs, borderWidth: 1, borderColor: AppColors.border, }, skipText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, }, cancelAllButton: { paddingVertical: Spacing.sm, }, cancelAllText: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.error, }, }); // 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 - enhanced display */} {sensor.error && ( {getErrorMessage(sensor.error).title} {getErrorMessage(sensor.error).description} )} {/* Action buttons for failed sensors - improved styling */} {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 [showErrorModal, setShowErrorModal] = useState(false); 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; // Find the current failed sensor for the modal const failedSensor = sensors.find(s => s.status === 'error'); // Show error modal when paused due to error useEffect(() => { if (isPaused && failedSensor) { // Small delay for better UX - let the card error state render first const timer = setTimeout(() => setShowErrorModal(true), 300); return () => clearTimeout(timer); } else { setShowErrorModal(false); } }, [isPaused, failedSensor]); const handleRetryFromModal = () => { setShowErrorModal(false); if (failedSensor && onRetry) { onRetry(failedSensor.deviceId); } }; const handleSkipFromModal = () => { setShowErrorModal(false); if (failedSensor && onSkip) { onSkip(failedSensor.deviceId); } }; const handleCancelAllFromModal = () => { setShowErrorModal(false); if (onCancelAll) { onCancelAll(); } }; // 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 )} {/* Error Action Modal */} ); } 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: { marginTop: Spacing.sm, padding: Spacing.md, backgroundColor: AppColors.errorLight, borderRadius: BorderRadius.md, borderLeftWidth: 3, borderLeftColor: AppColors.error, }, errorHeader: { flexDirection: 'row', alignItems: 'center', gap: Spacing.xs, marginBottom: Spacing.xs, }, errorTitle: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.error, }, errorText: { fontSize: FontSizes.xs, color: AppColors.textSecondary, lineHeight: 18, }, actionButtons: { flexDirection: 'row', marginTop: Spacing.md, gap: Spacing.sm, }, retryButton: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: Spacing.sm, paddingHorizontal: Spacing.md, backgroundColor: AppColors.primary, borderRadius: BorderRadius.md, gap: Spacing.xs, ...Shadows.xs, }, retryText: { fontSize: FontSizes.sm, fontWeight: FontWeights.semibold, color: AppColors.white, }, skipButton: { flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: Spacing.sm, paddingHorizontal: Spacing.md, backgroundColor: AppColors.surfaceSecondary, borderRadius: BorderRadius.md, borderWidth: 1, borderColor: AppColors.border, gap: Spacing.xs, }, skipText: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.textSecondary, }, cancelAllButton: { alignItems: 'center', paddingVertical: Spacing.sm, marginTop: Spacing.md, }, cancelAllText: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.error, }, });