import { NativeStackScreenProps } from '@react-navigation/native-stack'; import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, TextInput, StyleSheet, ScrollView, TouchableOpacity, Alert, ActivityIndicator, Share, Image, ToastAndroid, } from 'react-native'; import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path import { apiCall } from '../../utils/ApiHelper'; // Adjust path import AppView from '../../components/AppView'; import { Picker } from '@react-native-picker/picker'; import colors from '../../components/colors'; import Clipboard from '@react-native-clipboard/clipboard'; import { CompositeScreenProps, useNavigation } from '@react-navigation/native'; import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; // Adjust path import { UPLOAD_GROUP_IMAGE } from '../../utils/ApiUrls'; import { launchImageLibrary } from 'react-native-image-picker'; import *as Keychain from 'react-native-keychain' import Ionicons from '@react-native-vector-icons/ionicons'; import { CustomButton } from '../../components/common/Custom'; import { StackNavigationProp } from '@react-navigation/stack'; // --- Type Definitions --- interface Participant { id: string; firstName: string; lastName: string | null; mobile: string; role: 'ORGANIZER' | 'USER'; } export interface GroupDetails { id: string; groupName: string; description: string; startDate: [number, number, number] | null; endDate: [number, number, number] | null; contributionAmount: number; minParticipants: number; maxParticipants: number; portalCharge: number | null; latePaymentCharge: number | null; lateFeeMode: 'DAILY' | 'FIXED' | null; lateFeeValue: number | null; cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; customCycleDays: number; withdrawalMethod: 'RANDOM' | 'SEQUENTIAL'; status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'APPROVED'; visibility: 'PUBLIC' | 'PRIVATE'; organizerId: string; totalParticipants: number; participants: Participant[]; imageBlob:string } export interface EditGroupPayload { groupName: string; description: string; maxParticipants: number; contributionAmount: number; customCycleDays: number | null; cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; latePaymentCharge: number; visibility: 'PUBLIC' | 'PRIVATE'; lateFeeMode: 'DAILY' | 'FIXED' | null; lateFeeValue: number | null; } // Correct props for a screen inside a nested Tab Navigator type EditGroupScreenProps = CompositeScreenProps< BottomTabScreenProps, NativeStackScreenProps >; type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null; type GroupManagementNavProp = StackNavigationProp; // --- Main Screen Component --- const EditGroupScreen: React.FC = ({ route }) => { const { groupId } = route.params; const navigation = useNavigation(); const [editingCard, setEditingCard] = useState(null); const [groupDetails, setGroupDetails] = useState(null); const [editableData, setEditableData] = useState({ groupName: '', description: '', maxParticipants: 0, contributionAmount: 0, customCycleDays: null, cycleType: 'MONTHLY', latePaymentCharge: 0, visibility: 'PRIVATE', lateFeeMode: null, lateFeeValue: null, }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [starting, setStarting] = useState(false); const [inviting, setInviting] = useState(false); const [error, setError] = useState(null); const [paymentStatus, setPaymentStatus] = useState(null); const [currentCycle, setCurrentCycle] = useState(null); const [selectedImage, setSelectedImage] = useState() const [imageUpdating, setImageUpdating] = useState(false) const initializeEditableData = (data: GroupDetails) => { setEditableData({ groupName: data.groupName, description: data.description, maxParticipants: data.maxParticipants, contributionAmount: data.contributionAmount, customCycleDays: data.customCycleDays, cycleType: data.cycleType, latePaymentCharge: data.latePaymentCharge ?? 0, visibility: data.visibility, lateFeeMode: data.lateFeeMode ?? null, lateFeeValue: data.lateFeeValue ?? null, }); }; const fetchGroupDetails = useCallback(async () => { setLoading(true); setError(null); try { const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string }; console.log("groupDetails", response) if (response.status === 'success' && response.data) { setGroupDetails(response.data); initializeEditableData(response.data); } else { throw new Error(response.message || 'Failed to fetch group data.'); } } catch (e: any) { setError(e.message); } finally { setLoading(false); } }, [groupId]); const getPaymentStatus = async()=>{ const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/group/${groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number ,status: boolean }; message?: string }; console.log("paymentStatus",response) if (response.status === 'success') { setPaymentStatus(response.data?.status || false); setCurrentCycle(response.data?.currentCycle || null); } } useEffect(() => { fetchGroupDetails(); getPaymentStatus(); }, [fetchGroupDetails]); const handleCancel = () => { if (groupDetails) { initializeEditableData(groupDetails); } setEditingCard(null); }; const handleSave = async () => { if (!groupDetails) return; if (editableData.maxParticipants < groupDetails.totalParticipants) { Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`); return; } setSaving(true); try { await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/groups/${groupDetails.id}/edit`, 'PUT', editableData); Alert.alert('Success', 'Group has been updated.'); setEditingCard(null); await fetchGroupDetails(); } catch (e: any) { Alert.alert('Save Error', e.message || 'An unexpected error occurred.'); } finally { setSaving(false); } }; const handleDistribute = async () => { navigation.navigate('RandomSelectUserScreen', { groupId: groupDetails?.id || '', cycleNumber: currentCycle || 0 }); }; const handleStartGroup = async () => { if (!groupDetails) return; Alert.alert( "Confirm Start", "Are you sure you want to start this group? This action cannot be undone.", [ { text: "Cancel", style: "cancel" }, { text: "Start", onPress: async () => { setStarting(true); try { await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/start?groupId=${groupDetails.id}`, 'GET'); Alert.alert('Success', 'Group has been started successfully.'); await fetchGroupDetails(); } catch (e: any) { Alert.alert('Start Error', e.message || 'An unexpected error occurred.'); } finally { setStarting(false); } }, style: 'destructive' } ] ); }; const handleInvite = async () => { if (!groupDetails) return; setInviting(true); try { const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string }; const inviteLink = response.data; if (inviteLink) { Clipboard.setString(inviteLink); Alert.alert('Copied!', 'Invitation link copied to clipboard.'); await Share.share({ message: `Join my group on CircleCollect! ${inviteLink}`, url: inviteLink, title: 'CircleCollect Group Invitation' }); } else { throw new Error('Could not generate invitation link.'); } } catch (error: any) { Alert.alert('Error', error.message || 'Failed to generate invitation.'); } finally { setInviting(false); } }; const handleUpdateField = (field: keyof EditGroupPayload, value: any) => { setEditableData(prev => ({ ...prev, [field]: value })); }; const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean, isEditable: boolean) => { const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" }; const getInputForField = () => { switch (field) { case 'groupName': return handleUpdateField('groupName', text)} />; case 'description': return handleUpdateField('description', text)} multiline />; case 'contributionAmount': return handleUpdateField('contributionAmount', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; case 'maxParticipants': return handleUpdateField('maxParticipants', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; case 'latePaymentCharge': return handleUpdateField('latePaymentCharge', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; case 'cycleType': return ( handleUpdateField('cycleType', itemValue)} style={styles.picker}> {['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => )} ); case 'customCycleDays': if (editableData.cycleType !== 'CUSTOM') return null; return handleUpdateField('customCycleDays', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; case 'visibility': return ( handleUpdateField('visibility', itemValue)} style={styles.picker}> ); default: return null; } }; return ( {label} {isEditing && isEditable ? ( {getInputForField()} ) : ( {displayValue} )} ); } const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => { const isEditingThisCard = editingCard === cardKey; return ( {title} {isEditable && !editingCard && ( setEditingCard(cardKey)} style={styles.editButton}> ✎ )} {fields.map(({ field, label, displayValue }) => renderField(field, label, displayValue(), isEditingThisCard) )} {isEditingThisCard && ( Cancel {saving ? : Save} )} ); } if (loading) return ; if (error) return {error}; if (!groupDetails) return null; const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED'; const isInvitable = groupDetails.status === 'APPROVED'; const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants; const pickImage = () => { launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => { if (response.didCancel) return; if (response.errorCode) { Alert.alert("Error", response.errorMessage || "Something went wrong"); } else if (response.assets && response.assets.length > 0) { const selectedImage = response.assets[0]; console.log(selectedImage?.uri) setSelectedImage(selectedImage?.uri) } }); }; const handleUpdateImage = async () => { try { const credentials = await Keychain.getGenericPassword(); if (!credentials) { Alert.alert("Error", "You are not logged in"); return; } const token = credentials.password; if (!selectedImage) { Alert.alert("Error", "Please select an image first"); return; } setImageUpdating(true); // Get file extension and mime type dynamically const fileExtension = selectedImage.split('.').pop(); const formData = new FormData(); formData.append("file", { uri: selectedImage, type: "image/jpeg", name: `group_${groupId}_profile.${fileExtension || "jpg"}`, }); const url = `${UPLOAD_GROUP_IMAGE}${groupId}/upload-image`; console.log("url : ", url) console.log("formdata : ", formData) // return const response = await fetch(url, { method: "PUT", headers: { "Content-Type": "multipart/form-data", "Authorization": `Bearer ${token}`, }, body: formData, }); console.log("image response : ", response) if(response?.status === 200){ fetchGroupDetails() ToastAndroid.show('Group uploaded successfully', ToastAndroid.SHORT) } } catch (error:any) { console.error("❌ Error updating group image:", error); Alert.alert("Error", error.message || "Could not update group image"); } finally { setImageUpdating(false); } }; const getProfileImage = () => { return selectedImage ? selectedImage : groupDetails?.imageBlob ? `data:image/jpeg;base64,${groupDetails.imageBlob}` : "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg"; }; // Helper function to get MIME type return ( Manage Group {isInvitable && ( {inviting ? : Invite} )} console.log("Error loading image")} /> {renderCard('Group Info', [ { field: 'groupName', label: 'Group Name', displayValue: () => groupDetails.groupName }, { field: 'description', label: 'Description', displayValue: () => groupDetails.description }, ], 'groupInfo')} {groupDetails.status === 'ACTIVE' ? renderCard('Financial Rules', [ { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` }, { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` }, { field: 'maxParticipants', label: 'Current Participants', displayValue: () => `${groupDetails.totalParticipants}` }, ], 'financialRules') : renderCard('Financial Rules', [ { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` }, { field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` }, { field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` }, { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` }, ], 'financialRules') } {renderCard('Configuration', [ { field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType }, ...(groupDetails.cycleType === 'CUSTOM' ? [{ field: 'customCycleDays' as keyof EditGroupPayload, label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays} days` }] : []), { field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility }, ], 'configuration')} Participants ({groupDetails.totalParticipants}) {groupDetails.participants.length > 0 ? ( groupDetails.participants.map(p => ( {`${p.firstName} ${p.lastName || ''}`.trim()} {p.role === 'ORGANIZER' && Organizer} {p.mobile} )) ) : ( No participants yet. )} {canStartGroup && ( {starting ? ( ) : ( Activate Group )} )} {paymentStatus && ( {starting ? ( ) : ( Distribute )} )} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F3F4F6' }, contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' }, headerContainer: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingTop: 16, paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', }, avatarContainer: { position: "relative", marginVertical: 16, }, avatar: { width: 120, height: 120, borderRadius: 60, borderWidth: 4, borderColor: "#e2e8f0", }, cameraIcon: { position: "absolute", // right: 0, bottom: 0, backgroundColor: colors.pastelBlue, borderRadius: 20, width: 40, height: 40, justifyContent: "center", alignItems: "center", borderWidth: 3, borderColor: "#fff", elevation: 2, }, headerTitle: { fontSize: 28, fontWeight: 'bold', color: '#111827' }, card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 }, cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingBottom: 10, marginBottom: 12, }, cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' }, editButton: { padding: 8 }, editButtonText: { color: colors.pastelPink, fontSize: 20, fontWeight: 'bold' }, fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingBottom: 12, marginBottom: 12 }, infoLabel: { fontSize: 16, color: '#6B7280', marginBottom: 4 }, infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' }, input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 14, fontSize: 16, color: '#111827' }, textArea: { height: 100, textAlignVertical: 'top' }, picker: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10 }, participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' }, participantName: { fontSize: 16, fontWeight: '600', color: '#111827' }, participantMobile: { fontSize: 14, color: '#6B7280', marginTop: 4 }, organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 }, organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' }, noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 20 }, cardActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#E5E7EB' }, button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center', justifyContent: 'center' }, saveButton: { backgroundColor: colors.pastelOrange }, cancelButton: { backgroundColor: '#E5E7EB' }, inviteButton: { backgroundColor: colors.primary }, buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' }, floatingButtonContainer: { position: 'absolute', bottom: 85, left: 20, right: 20, alignItems: 'center', zIndex: 1, }, activateButton: { backgroundColor: colors.pastelOrange, borderRadius: 14, paddingVertical: 16, paddingHorizontal: 32, elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 4, width: '90%', alignItems: 'center', }, activateButtonText: { color: '#000000ff', fontSize: 18, fontWeight: 'bold', }, }); export default EditGroupScreen;