import React, { useState, useEffect, useCallback, useRef } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, RefreshControl, Image, Modal, TextInput, Alert, KeyboardAvoidingView, Platform, ActivityIndicator, } from 'react-native'; import { WebView } from 'react-native-webview'; import { useLocalSearchParams, router } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import { SafeAreaView } from 'react-native-safe-area-context'; import * as ImagePicker from 'expo-image-picker'; import { api } from '@/services/api'; import { useBeneficiary } from '@/contexts/BeneficiaryContext'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { FullScreenError } from '@/components/ui/ErrorMessage'; import { useToast } from '@/components/ui/Toast'; import { DevModeToggle } from '@/components/ui/DevModeToggle'; import MockDashboard from '@/components/MockDashboard'; import { AppColors, BorderRadius, FontSizes, Spacing, FontWeights, Shadows, AvatarSizes, } from '@/constants/theme'; import type { Beneficiary } from '@/types'; import { hasBeneficiaryDevices, hasActiveSubscription, shouldShowSubscriptionWarning, } from '@/services/BeneficiaryDetailController'; // WebView Dashboard URL - opens specific deployment directly const getDashboardUrl = (deploymentId?: number) => { const baseUrl = 'https://react.eluxnetworks.net/dashboard'; return deploymentId ? `${baseUrl}/${deploymentId}` : `${baseUrl}/21`; }; // Ferdinand's default deployment ID (has sensor data) const FERDINAND_DEPLOYMENT_ID = 21; export default function BeneficiaryDetailScreen() { const { id } = useLocalSearchParams<{ id: string }>(); const { setCurrentBeneficiary } = useBeneficiary(); const toast = useToast(); const [beneficiary, setBeneficiary] = useState(null); const [isLoading, setIsLoading] = useState(true); const [isRefreshing, setIsRefreshing] = useState(false); const [error, setError] = useState(null); const [isMenuVisible, setIsMenuVisible] = useState(false); const [showWebView, setShowWebView] = useState(false); const [isWebViewReady, setIsWebViewReady] = useState(false); const [authToken, setAuthToken] = useState(null); const [userName, setUserName] = useState(null); const [userId, setUserId] = useState(null); // Edit modal state const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [editForm, setEditForm] = useState({ name: '', address: '', avatar: undefined as string | undefined }); const webViewRef = useRef(null); // Load legacy token for WebView dashboard useEffect(() => { const loadLegacyToken = async () => { try { const token = await api.getLegacyToken(); const name = await api.getLegacyUserName(); const id = await api.getLegacyUserId(); setAuthToken(token); setUserName(name); setUserId(id); setIsWebViewReady(true); } catch (err) { console.log('[BeneficiaryDetail] Legacy token not available'); setIsWebViewReady(true); } }; loadLegacyToken(); }, []); const loadBeneficiary = useCallback(async (showLoadingIndicator = true) => { if (!id) return; if (showLoadingIndicator && !isRefreshing) { setIsLoading(true); } setError(null); try { const response = await api.getWellNuoBeneficiary(parseInt(id, 10)); if (response.ok && response.data) { const data = response.data; setBeneficiary(data); setCurrentBeneficiary(data); // WATERFALL REDIRECT LOGIC: // 1. First check devices (highest priority) if (!hasBeneficiaryDevices(data)) { const status = data.equipmentStatus; if (status && ['ordered', 'shipped', 'delivered'].includes(status)) { // Equipment is ordered/shipped/delivered → show status router.replace(`/(tabs)/beneficiaries/${id}/equipment-status`); return; } else { // No devices and no order → need to purchase router.replace(`/(tabs)/beneficiaries/${id}/purchase`); return; } } // 2. Then check subscription (only if devices exist) if (!hasActiveSubscription(data)) { router.replace(`/(tabs)/beneficiaries/${id}/subscription`); return; } // 3. All good → show Dashboard (this screen) } else { setError(response.error?.message || 'Failed to load beneficiary'); } } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred'); } finally { setIsLoading(false); setIsRefreshing(false); } }, [id, setCurrentBeneficiary, isRefreshing]); useEffect(() => { loadBeneficiary(); }, [loadBeneficiary]); const handleRefresh = useCallback(() => { setIsRefreshing(true); loadBeneficiary(false); }, [loadBeneficiary]); const handleEditPress = () => { if (beneficiary) { setEditForm({ name: beneficiary.name || '', address: beneficiary.address || '', avatar: beneficiary.avatar, }); setIsEditModalVisible(true); } }; const handlePickAvatar = async () => { const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync(); if (status !== 'granted') { toast.error('Permission needed', 'Please allow access to your photo library.'); return; } const result = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], allowsEditing: true, aspect: [1, 1], quality: 0.5, }); if (!result.canceled && result.assets[0]) { setEditForm(prev => ({ ...prev, avatar: result.assets[0].uri })); } }; const handleSaveEdit = async () => { if (!editForm.name.trim() || !id) { toast.error('Error', 'Name is required'); return; } try { const response = await api.updateWellNuoBeneficiary(parseInt(id, 10), { name: editForm.name.trim(), address: editForm.address.trim() || undefined, }); if (response.ok) { setIsEditModalVisible(false); toast.success('Saved', 'Profile updated successfully'); loadBeneficiary(false); } else { toast.error('Error', response.error?.message || 'Failed to save changes.'); } } catch (err) { toast.error('Error', 'Failed to save changes.'); } }; const handleDeleteBeneficiary = () => { if (!id) return; Alert.alert( 'Remove Beneficiary', `Are you sure you want to remove ${beneficiary?.name}? This action cannot be undone.`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Remove', style: 'destructive', onPress: async () => { try { const response = await api.deleteBeneficiary(parseInt(id, 10)); if (response.ok) { toast.success('Removed', 'Beneficiary has been removed'); router.replace('/(tabs)'); } else { toast.error('Error', response.error?.message || 'Failed to remove beneficiary'); } } catch (err) { toast.error('Error', 'Failed to remove beneficiary'); } }, }, ] ); }; // JavaScript to inject token into localStorage for WebView const injectedJavaScript = authToken ? ` (function() { try { var authData = { username: '${userName || ''}', token: '${authToken}', user_id: ${userId || 'null'} }; localStorage.setItem('auth2', JSON.stringify(authData)); console.log('Auth data injected:', authData.username); } catch(e) { console.error('Failed to inject token:', e); } })(); true; ` : ''; if (isLoading) { return ; } if (error || !beneficiary) { return ( loadBeneficiary()} /> ); } return ( {/* Header */} router.replace('/(tabs)')}> {/* Avatar + Name */} {beneficiary.avatar && beneficiary.avatar.trim() !== '' && !beneficiary.avatar.includes('placeholder') ? ( ) : ( {beneficiary.name.charAt(0).toUpperCase()} )} {beneficiary.name} setIsMenuVisible(!isMenuVisible)}> {/* Dropdown Menu */} {isMenuVisible && ( { setIsMenuVisible(false); handleEditPress(); }} > Edit { setIsMenuVisible(false); router.push(`/(tabs)/beneficiaries/${id}/share`); }} > Access { setIsMenuVisible(false); router.push(`/(tabs)/beneficiaries/${id}/subscription`); }} > Subscription { setIsMenuVisible(false); router.push(`/(tabs)/beneficiaries/${id}/equipment`); }} > Equipment { setIsMenuVisible(false); handleDeleteBeneficiary(); }} > Remove )} {/* Backdrop to close menu */} {isMenuVisible && ( setIsMenuVisible(false)} /> )} {/* DEBUG PANEL */} {__DEV__ && ( DEBUG INFO (tap to copy) { const debugData = JSON.stringify({ hasDevices: hasBeneficiaryDevices(beneficiary), equipmentStatus: beneficiary.equipmentStatus, subscription: beneficiary.subscription, deviceId: beneficiary.device_id, devices: beneficiary.devices, }, null, 2); console.log('DEBUG DATA:', debugData); }}> hasDevices: {String(hasBeneficiaryDevices(beneficiary))} equipmentStatus: {beneficiary.equipmentStatus || 'none'} subscription: {beneficiary.subscription ? JSON.stringify(beneficiary.subscription) : 'none'} device_id: {beneficiary.device_id || 'null'} devices: {beneficiary.devices ? String(beneficiary.devices) : '0'} )} {/* Dashboard Content */} {shouldShowSubscriptionWarning(beneficiary) && ( Subscription expiring soon )} {/* Developer Toggle */} {/* Content area - WebView or MockDashboard */} {showWebView ? ( isWebViewReady && authToken ? ( { console.log('[WebView] Message:', event.nativeEvent.data); }} renderLoading={() => ( Loading dashboard... )} /> ) : ( {isWebViewReady ? 'Loading dashboard...' : 'Connecting to sensors...'} ) ) : ( } > )} {/* Edit Modal */} Edit Profile setIsEditModalVisible(false)}> {/* Avatar */} {editForm.avatar ? ( ) : ( )} {/* Name */} Name setEditForm(prev => ({ ...prev, name: text }))} placeholder="Full name" placeholderTextColor={AppColors.textMuted} /> {/* Address */} Address setEditForm(prev => ({ ...prev, address: text }))} placeholder="Street address" placeholderTextColor={AppColors.textMuted} multiline numberOfLines={3} /> setIsEditModalVisible(false)} > Cancel Save ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: AppColors.background, }, // Header header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, borderBottomWidth: 1, borderBottomColor: AppColors.border, backgroundColor: AppColors.surface, zIndex: 10, }, headerButton: { width: 36, height: 36, borderRadius: BorderRadius.md, justifyContent: 'center', alignItems: 'center', }, headerCenter: { flexDirection: 'row', alignItems: 'center', gap: Spacing.sm, }, headerAvatar: { width: AvatarSizes.sm, height: AvatarSizes.sm, borderRadius: AvatarSizes.sm / 2, backgroundColor: AppColors.primary, justifyContent: 'center', alignItems: 'center', }, headerAvatarText: { fontSize: FontSizes.base, fontWeight: FontWeights.bold, color: AppColors.white, }, headerAvatarImage: { width: AvatarSizes.sm, height: AvatarSizes.sm, borderRadius: AvatarSizes.sm / 2, }, headerTitle: { fontSize: FontSizes.lg, fontWeight: FontWeights.semibold, color: AppColors.textPrimary, }, // Dropdown Menu dropdownMenu: { position: 'absolute', top: 44, right: 0, backgroundColor: AppColors.surface, borderRadius: BorderRadius.lg, minWidth: 160, ...Shadows.lg, zIndex: 100, }, dropdownItem: { flexDirection: 'row', alignItems: 'center', padding: Spacing.md, gap: Spacing.sm, }, dropdownItemText: { fontSize: FontSizes.base, color: AppColors.textPrimary, }, dropdownItemDanger: { borderTopWidth: 1, borderTopColor: AppColors.border, }, dropdownItemTextDanger: { color: AppColors.error, }, menuBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 5, }, // Debug Panel debugPanel: { backgroundColor: '#FFF9C4', padding: Spacing.sm, borderBottomWidth: 1, borderBottomColor: '#F9A825', }, debugTitle: { fontSize: FontSizes.xs, fontWeight: FontWeights.bold, color: '#F57F17', marginBottom: 4, }, debugText: { fontSize: FontSizes.xs, color: '#5D4037', fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', }, // Dashboard dashboardContainer: { flex: 1, }, subscriptionWarning: { flexDirection: 'row', alignItems: 'center', backgroundColor: AppColors.warningLight, paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, gap: Spacing.xs, }, subscriptionWarningText: { fontSize: FontSizes.sm, color: AppColors.warning, fontWeight: FontWeights.medium, }, devToggleSection: { paddingHorizontal: Spacing.md, paddingVertical: Spacing.sm, backgroundColor: AppColors.surface, borderBottomWidth: 1, borderBottomColor: AppColors.border, }, dashboardContent: { flex: 1, }, webView: { flex: 1, }, webViewLoading: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: Spacing.md, }, webViewLoadingText: { fontSize: FontSizes.base, color: AppColors.textSecondary, }, nativeScrollView: { flex: 1, }, nativeScrollContent: { padding: Spacing.md, }, // Edit Modal modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'flex-end', }, modalContainer: { backgroundColor: AppColors.surface, borderTopLeftRadius: BorderRadius.xl, borderTopRightRadius: BorderRadius.xl, maxHeight: '80%', }, modalHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', padding: Spacing.md, borderBottomWidth: 1, borderBottomColor: AppColors.border, }, modalTitle: { fontSize: FontSizes.lg, fontWeight: FontWeights.bold, color: AppColors.textPrimary, }, modalContent: { padding: Spacing.lg, }, avatarPicker: { alignSelf: 'center', marginBottom: Spacing.lg, }, avatarPickerImage: { width: 100, height: 100, borderRadius: 50, }, avatarPickerPlaceholder: { width: 100, height: 100, borderRadius: 50, backgroundColor: AppColors.surfaceSecondary, justifyContent: 'center', alignItems: 'center', }, avatarPickerBadge: { position: 'absolute', bottom: 0, right: 0, width: 28, height: 28, borderRadius: 14, backgroundColor: AppColors.primary, justifyContent: 'center', alignItems: 'center', borderWidth: 2, borderColor: AppColors.surface, }, inputGroup: { marginBottom: Spacing.md, }, inputLabel: { fontSize: FontSizes.sm, fontWeight: FontWeights.medium, color: AppColors.textSecondary, marginBottom: Spacing.xs, }, textInput: { backgroundColor: AppColors.surfaceSecondary, borderRadius: BorderRadius.md, padding: Spacing.md, fontSize: FontSizes.base, color: AppColors.textPrimary, }, textArea: { minHeight: 80, textAlignVertical: 'top', }, modalFooter: { flexDirection: 'row', padding: Spacing.md, gap: Spacing.md, borderTopWidth: 1, borderTopColor: AppColors.border, }, cancelButton: { flex: 1, padding: Spacing.md, borderRadius: BorderRadius.md, backgroundColor: AppColors.surfaceSecondary, alignItems: 'center', }, cancelButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.textSecondary, }, saveButton: { flex: 1, padding: Spacing.md, borderRadius: BorderRadius.md, backgroundColor: AppColors.primary, alignItems: 'center', }, saveButtonText: { fontSize: FontSizes.base, fontWeight: FontWeights.semibold, color: AppColors.white, }, });