import React, { useState, useEffect, useRef, useCallback } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Image, ActivityIndicator, ToastAndroid, FlatList, Modal, Platform, TouchableWithoutFeedback, Keyboard, TextInput, ScrollView, Dimensions, KeyboardAvoidingView, Share, RefreshControl, } from 'react-native'; import { useRoute, useNavigation } from '@react-navigation/native'; import { Picker } from '@react-native-picker/picker'; import Clipboard from '@react-native-clipboard/clipboard'; import { launchImageLibrary } from 'react-native-image-picker'; import * as Keychain from 'react-native-keychain'; import LucideIcon from '../../components/common/LucideIcon'; import { responsiveFontSize } from 'react-native-responsive-dimensions'; import AppView from '../../components/AppView'; import { useTheme } from '../../hooks/ui/useTheme'; import { useFetch } from '../../hooks/core/useFetch'; import { useUpdate } from '../../hooks/core/useUpdate'; import { useInvite } from '../../hooks/core/useInvite'; import { BASE_URL, UPLOAD_GROUP_IMAGE } from '../../utils/ApiUrls'; import FontsFamily from '../../constents/FontsFamily'; import SkeletonLoader from '../../components/common/SkeletonLoader'; import WinnerUser from './WinnerUser'; const { width } = Dimensions.get('window'); // --- TYPE DEFINITIONS --- interface Participant { id: string; firstName: string; lastName: string | null; mobile: string; role: 'ORGANIZER' | 'USER'; } export interface EditGroupPayload { groupName: string; description: string; maxParticipants: number; minParticipants: number; lateFeeMode: 'DAILY' | 'FIXED' | string; lateFeeValue: number; latePaymentCharge: number; visibility: 'PUBLIC' | 'PRIVATE'; contributionAmount: number; cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM' | 'HALF_YEARLY' | 'YEARLY'; entryFee: number; customCycleDays: number | null; } type EditSection = 'groupInfo' | 'financialRules' | 'configuration' | null; const EditGroupScreen: React.FC = () => { const theme = useTheme(); const styles = React.useMemo(() => getStyles(theme), [theme]); const route = useRoute(); const navigation = useNavigation(); const { groupId } = route?.params || {}; // State const [activeSection, setActiveSection] = useState(null); const [editableData, setEditableData] = useState(null); const [originalData, setOriginalData] = useState(null); const [changedFields, setChangedFields] = useState>(new Set()); const [selectedImage, setSelectedImage] = useState(null); const [imageUpdating, setImageUpdating] = useState(false); const [selectedUserModal, setSelectedUserModel] = useState(false); const [isActivating, setIsActivating] = useState(false); const [showInviteTooltip, setShowInviteTooltip] = useState(false); // --- DATA FETCHING --- const { data: groupDetailsRes, isLoading: isGroupLoading, isRefetching: isRefetchingGroup, refetch: refetchGroup, } = useFetch( `${BASE_URL}fund/group/details?groupId=${groupId}`, undefined, { enabled: !!groupId } ); const groupDetails = groupDetailsRes?.data; // Initialize data useEffect(() => { if (groupDetails) { const newData: EditGroupPayload = { groupName: groupDetails.groupName || '', description: groupDetails.description || '', maxParticipants: groupDetails.maxParticipants || 0, minParticipants: groupDetails.minParticipants || 0, contributionAmount: groupDetails.contributionAmount || 0, customCycleDays: groupDetails.customCycleDays || 0, cycleType: groupDetails.cycleType || 'MONTHLY', latePaymentCharge: groupDetails.latePaymentCharge ?? 0, visibility: groupDetails.visibility || 'PUBLIC', lateFeeMode: groupDetails.lateFeeMode ?? null, lateFeeValue: groupDetails.lateFeeValue ?? 0, entryFee: groupDetails.entryFee ?? 0, }; setOriginalData(newData); setEditableData(newData); setChangedFields(new Set()); if (groupDetails.status === 'APPROVED') { setShowInviteTooltip(true); const timer = setTimeout(() => setShowInviteTooltip(false), 5000); return () => clearTimeout(timer); } } // DEBUG: Log Token const logToken = async () => { try { const credentials = await Keychain.getGenericPassword(); if (credentials) { console.log("🔐 AUTH TOKEN:", credentials.password); } else { console.log("🔐 No credentials stored"); } } catch (error) { console.error("Error retrieving token:", error); } }; logToken(); }, [groupDetails]); const groupStatusFromDetails = groupDetails?.status; const { data: paymentStatusRes, refetch: refetchPaymentStatus } = useFetch( `${BASE_URL}payments/group/${groupId}/payment/paid`, undefined, { enabled: !!groupId && (groupStatusFromDetails === 'ACTIVE' || groupStatusFromDetails === 'DELAY') } ); const paymentStatus = paymentStatusRes?.data; const { mutate: generateInvite, isPending: isInviting } = useInvite({ onSuccess: (res) => { if (res.data) { Clipboard.setString(res.data); ToastAndroid.show('Invite link copied', ToastAndroid.SHORT); Share.share({ message: `Join my group: ${res.data}` }); } }, onError: (error: any) => { ToastAndroid.show(error?.message || 'Failed to generate invite', ToastAndroid.SHORT); } }); // --- MUTATIONS --- const { mutate: updateGroup, isPending: isSaving } = useUpdate( `${BASE_URL}fund/groups/${groupId}/edit`, { onSuccess: () => { ToastAndroid.show('Group updated successfully', ToastAndroid.SHORT); setActiveSection(null); refetchGroup(); }, onError: (err: any) => { ToastAndroid.show(err?.message || 'Failed to update group', ToastAndroid.SHORT); }, } ); const activateGroup = async () => { setIsActivating(true); try { const credentials = await Keychain.getGenericPassword(); const token = credentials ? credentials.password : ''; const response = await fetch(`${BASE_URL}fund/group/start?groupId=${groupId}`, { method: 'GET', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); if (response.ok) { ToastAndroid.show('Group activated successfully', ToastAndroid.SHORT); refetchGroup(); } else { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.message || 'Activation failed'); } } catch (error: any) { ToastAndroid.show(error?.message || 'Activation failed', ToastAndroid.SHORT); } finally { setIsActivating(false); } }; // --- HANDLERS --- const handleSave = () => { if (!editableData || !groupDetails) return; if (editableData.maxParticipants < groupDetails.totalParticipants) { ToastAndroid.show('Capacity cannot be less than current participants', ToastAndroid.SHORT); return; } // Construct payload strictly matching user requirements const payloadData: EditGroupPayload = { groupName: editableData.groupName, description: editableData.description, maxParticipants: Number(editableData.maxParticipants || 0), minParticipants: Number(editableData.minParticipants || 0), lateFeeMode: editableData.lateFeeMode || 'DAILY', lateFeeValue: Number(editableData.lateFeeValue || 0), latePaymentCharge: Number(editableData.latePaymentCharge || 0), visibility: editableData.visibility || 'PUBLIC', contributionAmount: Number(editableData.contributionAmount || 0), cycleType: editableData.cycleType || 'MONTHLY', entryFee: Number(editableData.entryFee || 0), customCycleDays: editableData.cycleType === 'CUSTOM' ? Number(editableData.customCycleDays || 0) : null, }; updateGroup(payloadData); }; const [refreshing, setRefreshing] = useState(false); const onRefresh = useCallback(() => { setRefreshing(true); const promises = [refetchGroup()]; if (groupStatusFromDetails === 'ACTIVE') { promises.push(refetchPaymentStatus()); } Promise.all(promises).finally(() => setRefreshing(false)); }, [refetchGroup, refetchPaymentStatus, groupStatusFromDetails]); const handleUpdateImage = async () => { if (!selectedImage) return; setImageUpdating(true); try { const credentials = await Keychain.getGenericPassword(); const token = credentials ? credentials.password : ''; const fileExtension = selectedImage.split('.').pop(); const formData = new FormData(); formData.append('file', { uri: selectedImage, type: 'image/jpeg', name: `group_${groupId}.${fileExtension || 'jpg'}`, } as any); const response = await fetch(`${UPLOAD_GROUP_IMAGE}${groupId}/upload-image`, { method: 'PUT', headers: { 'Content-Type': 'multipart/form-data', Authorization: `Bearer ${token}`, }, body: formData, }); if (response.status === 200) { ToastAndroid.show('Image updated', ToastAndroid.SHORT); refetchGroup(); setSelectedImage(null); } else { throw new Error('Upload failed'); } } catch (e) { ToastAndroid.show('Failed to upload image', ToastAndroid.SHORT); } finally { setImageUpdating(false); } }; const handleInvite = () => { generateInvite(groupId); }; const pickImage = () => { launchImageLibrary({ mediaType: 'photo', quality: 0.5 }, (res) => { if (res.assets?.[0]?.uri) setSelectedImage(res.assets[0].uri); }); }; const handleDistribute = () => { navigation.navigate('RandomSelectUserScreen', { groupId, cycleNumber: paymentStatus?.currentCycle || 0 }); }; const handleFieldChange = (field: keyof EditGroupPayload, value: any) => { if (!editableData) return; const newData = { ...editableData, [field]: value }; setEditableData(newData); const newChangedFields = new Set(changedFields); if (originalData && originalData[field] !== value) { newChangedFields.add(field); } else { newChangedFields.delete(field); } setChangedFields(newChangedFields); }; // --- RENDER HELPERS --- const renderField = (field: keyof EditGroupPayload, label: string) => { if (!editableData) return null; const isNum = ['contributionAmount', 'latePaymentCharge', 'maxParticipants', 'customCycleDays', 'minParticipants', 'entryFee', 'lateFeeValue'].includes(field); return ( {label} {field === 'cycleType' || field === 'visibility' || field === 'lateFeeMode' ? ( {(field === 'cycleType' ? ['MONTHLY', 'QUARTERLY', 'HALF_YEARLY', 'YEARLY', 'CUSTOM'] : field === 'lateFeeMode' ? ['DAILY', 'MONTHLY'] : ['PUBLIC', 'PRIVATE'] ).map((option) => ( handleFieldChange(field, option)} > {option} ))} ) : ( handleFieldChange(field, isNum ? Number(text.replace(/[^0-9.]/g, '')) : text)} keyboardType={isNum ? 'decimal-pad' : 'default'} multiline={field === 'description'} numberOfLines={field === 'description' ? 3 : 1} textAlignVertical={field === 'description' ? 'top' : 'center'} /> )} ); }; // --- LIST SECTIONS --- const SECTIONS = [ { id: 'cover', type: 'cover' }, { id: 'stats', type: 'stats' }, { id: 'info', type: 'card', title: 'Group Info', section: 'groupInfo' as EditSection }, { id: 'financial', type: 'card', title: 'Financial Rules', section: 'financialRules' as EditSection }, { id: 'config', type: 'card', title: 'Configuration', section: 'configuration' as EditSection }, { id: 'participants', type: 'participants' }, { id: 'actions', type: 'actions' }, ]; const renderItem = ({ item }: { item: any }) => { if (!groupDetails) return null; if (item.type === 'cover') { const coverImageUri = selectedImage || (groupDetails.imageUrl ? groupDetails.imageUrl : `https://api.dicebear.com/7.x/shapes/png?seed=${encodeURIComponent(groupDetails.groupName)}`); return ( navigation.goBack()} style={styles.iconBtn}> Manage Group {groupDetails.status === 'APPROVED' ? ( {showInviteTooltip && ( setShowInviteTooltip(false)} style={styles.inviteTooltip} > Invite members )} ) : ( )} {groupDetails.groupName} {groupDetails.description && ( {groupDetails.description} )} {groupDetails.status} {groupDetails.visibility} {selectedImage && ( Upload Image {imageUpdating ? : } )} ); } if (item.type === 'stats') { return ( {groupDetails.totalParticipants} Participants ₹{groupDetails.contributionAmount} Contribution {groupDetails.cycleType} Cycle ); } if (item.type === 'card') { const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED'; const fields = item.section === 'groupInfo' ? ['groupName', 'description', 'visibility'] : item.section === 'financialRules' ? ['contributionAmount', 'entryFee', 'latePaymentCharge', 'lateFeeMode'] : ['maxParticipants', 'minParticipants', 'cycleType', ...(groupDetails.customCycleDays ? ['customCycleDays'] : [])]; const getDisplayValue = (field: string) => { const val = groupDetails[field as keyof typeof groupDetails]; if (field.includes('Amount') || field.includes('Fee') || field.includes('Charge') || field.includes('Value')) return `₹${val ?? 0}`; return String(val ?? '-'); }; return ( isEditable && setActiveSection(item.section)} activeOpacity={isEditable ? 0.7 : 1} > {item.title} {isEditable && } {fields.map((f: string) => ( {f.replace(/([A-Z])/g, ' $1').trim()}: {getDisplayValue(f)} ))} ); } if (item.type === 'participants') { return ( Participants ({groupDetails.participants.length}) {groupDetails.participants.slice(0, 3).map((p: any) => ( {p.firstName[0]} {p.firstName} {p.lastName} {p.role} ))} {groupDetails.participants.length > 3 && ( View All Participants )} ); } if (item.type === 'actions') { const isApproved = groupDetails.status === 'APPROVED'; const notEnoughParticipants = groupDetails.totalParticipants < groupDetails.minParticipants; return ( {isApproved && notEnoughParticipants && ( To start the group, a minimum of {groupDetails.minParticipants} participants are needed. )} {paymentStatus?.cycleStatus === 'PENDING_SELECTION' && ( Distribute Funds )} {paymentStatus?.cycleStatus === 'READY_FOR_WITHDRAWAL' && paymentStatus?.selectedUser && ( setSelectedUserModel(true)} style={styles.secondaryBtn}> View Winner )} ); } return null; }; if (isGroupLoading) { return ( ); } if (!groupDetails) return null; return ( item.id} contentContainerStyle={{ paddingBottom: 150 }} showsVerticalScrollIndicator={false} refreshControl={ } /> {/* Floating Activate Button */} {groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants && ( activateGroup()} style={styles.floatingActivateBtn} disabled={isActivating} > {isActivating ? ( ) : ( <> Activate Group )} )} {/* --- EDIT MODAL --- */} setActiveSection(null)} > {activeSection === 'groupInfo' ? 'Group Information' : activeSection === 'financialRules' ? 'Financial Rules' : 'Configuration'} setActiveSection(null)} style={styles.closeBtn}> {activeSection === 'groupInfo' && ( <> {renderField('groupName', 'Group Name')} {renderField('description', 'Description')} {renderField('visibility', 'Visibility')} )} {activeSection === 'financialRules' && ( <> {renderField('contributionAmount', 'Contribution Amount')} {renderField('entryFee', 'Entry Fee')} {renderField('latePaymentCharge', 'Late Payment Charge')} {renderField('lateFeeMode', 'Late Fee Mode')} )} {activeSection === 'configuration' && ( <> {renderField('maxParticipants', 'Max Participants')} {renderField('minParticipants', 'Min Participants')} {renderField('cycleType', 'Payment Cycle')} {editableData?.cycleType === 'CUSTOM' && renderField('customCycleDays', 'Cycle Days')} )} {isSaving ? : Save Changes} {selectedUserModal && ( setSelectedUserModel(false)} /> )} ); }; const getStyles = (theme: any) => StyleSheet.create({ // Cover coverSection: { height: 280, position: 'relative' }, coverImage: { width: '100%', height: '100%', resizeMode: 'cover' }, coverOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.4)' }, coverHeader: { position: 'absolute', top: 30, left: 20, right: 20, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, iconBtn: { padding: 8, backgroundColor: 'rgba(255,255,255,0.2)', borderRadius: 20 }, headerTitle: { color: '#fff', fontSize: 18, fontFamily: FontsFamily.nunitoBold }, coverContent: { position: 'absolute', bottom: 30, left: 20, right: 20 }, groupNameTitle: { color: '#fff', fontSize: 24, fontFamily: FontsFamily.nunitoBold, marginBottom: 8 }, descContainer: { backgroundColor: 'rgba(255,255,255,0.15)', padding: 10, borderRadius: 8, borderLeftWidth: 3, borderLeftColor: '#fff', marginBottom: 12 }, descText: { color: '#eee', fontSize: 14, fontFamily: FontsFamily.nunitoRegular }, badgesRow: { flexDirection: 'row', gap: 10 }, badge: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 10, paddingVertical: 4, borderRadius: 12 }, badgeText: { color: '#fff', fontSize: 12, fontWeight: 'bold' }, cameraBtn: { position: 'absolute', bottom: -20, right: 30, width: 44, height: 44, borderRadius: 22, backgroundColor: '#fff', elevation: 4, justifyContent: 'center', alignItems: 'center' }, uploadFloatBtn: { position: 'absolute', bottom: 40, right: 30, width: 50, height: 50, borderRadius: 25, elevation: 5, justifyContent: 'center', alignItems: 'center' }, uploadFloatWrapper: { position: 'absolute', bottom: 40, right: 30, alignItems: 'center' }, uploadTooltip: { position: 'absolute', bottom: 58, right: 0, paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8, elevation: 6, zIndex: 10, minWidth: 100, alignItems: 'center' }, uploadTooltipText: { fontSize: 12, fontFamily: FontsFamily.nunitoBold }, uploadTooltipArrow: { position: 'absolute', bottom: -5, width: 0, height: 0, borderLeftWidth: 6, borderRightWidth: 6, borderTopWidth: 6, borderLeftColor: 'transparent', borderRightColor: 'transparent' }, uploadFloatBtnInner: { width: 50, height: 50, borderRadius: 25, elevation: 5, justifyContent: 'center', alignItems: 'center' }, // Stats statsContainer: { flexDirection: 'row', backgroundColor: theme.colors.card, margin: 20, marginTop: 30, borderRadius: 16, padding: 20, elevation: 2, justifyContent: 'space-between', alignItems: 'center' }, statBox: { alignItems: 'center', flex: 1 }, statValue: { fontSize: 18, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary }, statLabel: { fontSize: 12, fontFamily: FontsFamily.nunitoRegular, color: theme.colors.textSecondary }, statDivider: { width: 1, height: 30, backgroundColor: '#eee' }, // Cards cardContainer: { backgroundColor: theme.colors.card, marginHorizontal: 20, marginBottom: 16, borderRadius: 16, padding: 16, elevation: 2 }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, paddingBottom: 10, borderBottomWidth: 1, borderBottomColor: '#f0f0f0' }, cardTitle: { fontSize: 16, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary }, cardBody: {}, cardFieldText: { fontSize: 14, color: theme.colors.textPrimary, marginBottom: 6, fontFamily: FontsFamily.nunitoMedium }, moreText: { fontSize: 12, color: theme.colors.textSecondary, marginTop: 4, fontStyle: 'italic' }, // Participants participantRow: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 }, avatarPlaceholder: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#6366F1', justifyContent: 'center', alignItems: 'center', marginRight: 12 }, pName: { fontSize: 16, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary }, pRole: { fontSize: 12, color: theme.colors.textSecondary }, viewAllBtn: { paddingVertical: 8, alignItems: 'center', borderTopWidth: 1, borderTopColor: '#f0f0f0', marginTop: 8 }, viewAllText: { color: theme.colors.primary, fontFamily: FontsFamily.nunitoBold }, // Actions actionsContainer: { padding: 20 }, primaryBtn: { backgroundColor: theme.colors.primary, padding: 16, borderRadius: 12, alignItems: 'center', elevation: 2, marginBottom: 12 }, btnText: { color: theme.colors.white, fontSize: 16, fontFamily: FontsFamily.nunitoBold }, secondaryBtn: { backgroundColor: '#f0f0f0', padding: 16, borderRadius: 12, alignItems: 'center' }, secondaryBtnText: { color: theme.colors.textPrimary, fontSize: 16, fontFamily: FontsFamily.nunitoBold }, // Modal modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end' }, modalContainer: { backgroundColor: theme.colors.background, borderTopLeftRadius: 24, borderTopRightRadius: 24, height: '80%', padding: 20 }, modalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }, modalTitle: { fontSize: 20, fontFamily: FontsFamily.nunitoBold, color: theme.colors.textPrimary }, closeBtn: { padding: 8, backgroundColor: '#f5f5f5', borderRadius: 20 }, // New Styles hintContainer: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FFFBEB', padding: 12, borderRadius: 12, marginBottom: 16, borderLeftWidth: 4, borderLeftColor: '#F59E0B' }, hintText: { color: '#B45309', fontSize: 13, fontFamily: FontsFamily.nunitoMedium, marginLeft: 8, flex: 1 }, floatingBtnContainer: { position: 'absolute', bottom: 90, left: 20, right: 20, alignItems: 'center', }, floatingActivateBtn: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', backgroundColor: theme.colors.primary, paddingVertical: 16, paddingHorizontal: 24, borderRadius: 16, width: '100%', elevation: 8, shadowColor: theme.colors.primary, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, }, // Invite & Tooltip inviteContainer: { position: 'relative', alignItems: 'center' }, inviteTooltip: { position: 'absolute', top: 45, right: -10, backgroundColor: '#d5d5d5ff', paddingHorizontal: 12, paddingVertical: 8, borderRadius: 8, width: 140, zIndex: 100, elevation: 10, }, tooltipText: { color: '#494949ff', fontSize: 11, fontFamily: FontsFamily.nunitoSemiBold, textAlign: 'center', }, tooltipArrow: { position: 'absolute', top: -6, right: 20, width: 0, height: 0, borderLeftWidth: 6, borderRightWidth: 6, borderBottomWidth: 6, borderLeftColor: 'transparent', borderRightColor: 'transparent', borderBottomColor: '#334155', }, floatingBtnText: { color: '#fff', fontSize: 18, fontFamily: FontsFamily.nunitoBold, letterSpacing: 0.5, }, modalContent: { flex: 1 }, modalFieldContainer: { marginBottom: 16 }, modalLabel: { fontSize: 14, color: theme.colors.textSecondary, marginBottom: 8, fontFamily: FontsFamily.nunitoBold }, modalInput: { borderWidth: 1, borderColor: '#e0e0e0', borderRadius: 12, padding: 14, fontSize: 16, color: theme.colors.textPrimary, backgroundColor: '#f9f9f9', fontFamily: FontsFamily.nunitoMedium }, chipContainer: { flexDirection: 'row', flexWrap: 'wrap', gap: 10 }, chip: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, backgroundColor: '#f0f0f0', borderWidth: 1, borderColor: '#e0e0e0' }, activeChip: { backgroundColor: theme.colors.primary, borderColor: theme.colors.primary }, chipText: { fontSize: 14, fontFamily: FontsFamily.nunitoMedium, color: theme.colors.textSecondary }, activeChipText: { color: theme.colors.white, fontFamily: FontsFamily.nunitoBold }, modalFooter: { borderTopWidth: 1, borderTopColor: '#eee', paddingTop: 16 }, saveBtn: { backgroundColor: theme.colors.primary, padding: 16, borderRadius: 12, alignItems: 'center' }, saveBtnText: { color: theme.colors.white, fontSize: 16, fontFamily: FontsFamily.nunitoBold } }); export default EditGroupScreen;