diff --git a/src/components/RequestCard.tsx b/src/components/RequestCard.tsx index 7d6e385..2878aef 100644 --- a/src/components/RequestCard.tsx +++ b/src/components/RequestCard.tsx @@ -1,10 +1,10 @@ // src/components/RequestCard.tsx import React, { FC } from 'react'; -import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native'; -import { Ionicons } from "@react-native-vector-icons/ionicons"; -import colors from './colors'; // Assuming your colors are defined here -import { JoinRequest } from '../types'; +import { View, Text, Image, TouchableOpacity, StyleSheet, Platform } from 'react-native'; +import { Ionicons } from "@react-native-vector-icons/ionicons"; // Assuming Expo for icons +import colors from './colors'; // Your color palette +import { JoinRequest } from '../types'; // Your type definition interface RequestCardProps { item: JoinRequest; @@ -15,94 +15,124 @@ interface RequestCardProps { export const RequestCard: FC = ({ item, onApprove, onDecline }) => { return ( - {/* Top section with user avatar and info */} + {/* User Information Section */} - {item.userName} - {item.mobile} + + + {item.mobile} + - {/* Middle section for the group name */} + {/* Group Information Section */} - - Wants to join the group: {item.groupName} - + Wants to join group + {item.groupName} - {/* Bottom section with action buttons */} + {/* Action Buttons Section */} - onDecline(item)}> - - Decline + onDecline(item)} + activeOpacity={0.7} + > + + Decline - onApprove(item)}> - {/* Corrected icon color and updated text color for better contrast */} - - Approve + onApprove(item)} + activeOpacity={0.8} + > + + Approve ); }; - - const styles = StyleSheet.create({ card: { - marginHorizontal: 12, + marginHorizontal: 16, + marginVertical: 12, backgroundColor: colors.white, - borderRadius: 16, - marginVertical: 8, - elevation: 5, - shadowColor: '#000', - shadowOpacity: 0.1, - shadowRadius: 10, - shadowOffset: { width: 0, height: 4 }, - padding: 8, + borderRadius: 24, + padding: 20, + ...Platform.select({ + ios: { + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.08, + shadowRadius: 12, + }, + android: { + elevation: 8, + shadowColor: '#00000020', // Softer shadow color for Android + }, + }), + borderWidth: 1, + borderColor: '#f0f0f0', }, userInfoSection: { flexDirection: 'row', alignItems: 'center', - paddingBottom: 6, }, avatar: { - width: 50, - height: 50, - borderRadius: 25, - marginRight: 12, + width: 60, + height: 60, + borderRadius: 30, + marginRight: 16, + borderWidth: 2, + borderColor: colors.pastelOrange, }, userInfo: { flex: 1, }, userName: { - fontSize: 17, + fontSize: 19, fontWeight: 'bold', color: colors.textDark, }, + userMetaContainer: { + flexDirection: 'row', + alignItems: 'center', + marginTop: 5, + }, userMeta: { fontSize: 14, color: colors.textLight, - marginTop: 4, + marginLeft: 8, }, groupInfoSection: { + marginTop: 18, paddingVertical: 16, + paddingHorizontal: 16, + backgroundColor: '#F7F8FA', + borderRadius: 16, + alignItems: 'center', }, requestText: { - fontSize: 15, - color: colors.textDark, + fontSize: 14, + color: colors.textLight, }, groupName: { - fontWeight: 'bold', + fontSize: 17, + fontWeight: '600', color: colors.textDark, + marginTop: 6, + textAlign: 'center', }, actionsSection: { flexDirection: 'row', - justifyContent: 'space-between', + marginTop: 20, gap: 12, }, actionButton: { @@ -110,21 +140,31 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - paddingVertical: 12, - borderRadius: 12, + paddingVertical: 16, + borderRadius: 16, }, declineButton: { - elevation:5, - backgroundColor:colors.white + backgroundColor: '#F1F3F6', }, approveButton: { - // Set background color as requested backgroundColor: colors.pastelOrange, - elevation:5, + ...Platform.select({ + ios: { + shadowColor: colors.pastelOrange, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, + }, + android: { + elevation: 6, + shadowColor: colors.pastelOrange, + }, + }), }, actionButtonText: { fontSize: 16, - fontWeight: '600', + fontWeight: 'bold', marginLeft: 8, + color: colors.textDark, }, }); diff --git a/src/screens/management/EditGroupScreen.tsx b/src/screens/management/EditGroupScreen.tsx index fe22591..d0d1861 100644 --- a/src/screens/management/EditGroupScreen.tsx +++ b/src/screens/management/EditGroupScreen.tsx @@ -1,5 +1,4 @@ -import { NativeStackScreenProps } from '@react-navigation/native-stack'; -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback } from "react"; import { View, Text, @@ -10,40 +9,34 @@ import { 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 { BASE_URL, 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'; -// import CustomModal from '../../components/common/CustomModal'; -import WinnerUser from './WinnerUser'; + Image +} from "react-native"; +import { NativeStackScreenProps } from "@react-navigation/native-stack"; +import { RootStackParamList } from "../../navigation/AppNavigator"; +import { apiCall } from "../../utils/ApiHelper"; +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"; +import { BASE_URL, UPLOAD_GROUP_IMAGE } from "../../utils/ApiUrls"; +import { launchImageLibrary, Asset } 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"; +import WinnerUser from "./WinnerUser"; +import CustomAlert from "../../components/CostomAlart"; - - -// --- Type Definitions --- interface Participant { id: string; firstName: string; lastName: string | null; mobile: string; - role: 'ORGANIZER' | 'USER'; + role: "ORGANIZER" | "USER"; } - - export interface GroupDetails { id: string; groupName: string; @@ -55,21 +48,19 @@ export interface GroupDetails { maxParticipants: number; portalCharge: number | null; latePaymentCharge: number | null; - lateFeeMode: 'DAILY' | 'FIXED' | null; + lateFeeMode: "DAILY" | "FIXED" | null; lateFeeValue: number | null; - cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; + cycleType: "QUARTERLY" | "MONTHLY" | "WEEKLY" | "CUSTOM"; customCycleDays: number; - withdrawalMethod: 'RANDOM' | 'SEQUENTIAL'; - status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'APPROVED'; - visibility: 'PUBLIC' | 'PRIVATE'; + 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; @@ -77,63 +68,68 @@ export interface EditGroupPayload { minParticipants?: number; contributionAmount: number; customCycleDays: number | null; - cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; + cycleType: "QUARTERLY" | "MONTHLY" | "WEEKLY" | "CUSTOM"; latePaymentCharge: number; - visibility: 'PUBLIC' | 'PRIVATE'; - lateFeeMode: 'DAILY' | 'FIXED' | null; + visibility: "PUBLIC" | "PRIVATE"; + lateFeeMode: "DAILY" | "FIXED" | null; lateFeeValue: number | null; } - - -// Correct props for a screen inside a nested Tab Navigator type EditGroupScreenProps = CompositeScreenProps< - BottomTabScreenProps, + BottomTabScreenProps, NativeStackScreenProps >; +type EditingCard = "groupInfo" | "financialRules" | "configuration" | null; +type GroupManagementNavProp = StackNavigationProp; -type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null; - - -type GroupManagementNavProp = StackNavigationProp; -// --- Main Screen Component --- const EditGroupScreen: React.FC = ({ route }) => { - const { groupId } = route?.params; - - - console.log("edit group screen groupId : ", route?.params) + const { groupId } = route?.params as { groupId: string }; const navigation = useNavigation(); - const [editingCard, setEditingCard] = useState(null); const [groupDetails, setGroupDetails] = useState(null); const [editableData, setEditableData] = useState({ - groupName: '', - description: '', + groupName: "", + description: "", maxParticipants: 0, contributionAmount: 0, customCycleDays: null, - cycleType: 'MONTHLY', + cycleType: "MONTHLY", latePaymentCharge: 0, - visibility: 'PRIVATE', + visibility: "PRIVATE", lateFeeMode: null, - lateFeeValue: null, + lateFeeValue: null }); - - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - const [starting, setStarting] = useState(false); - const [inviting, setInviting] = useState(false); + 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 [selectedUser, setSelectedUser] = useState() as any - const [selectedUserModal, setSelectedUserModel] = useState(false) + const [selectedImage, setSelectedImage] = useState(null); + const [imageUpdating, setImageUpdating] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + const [selectedUserModal, setSelectedUserModel] = useState(false); + + // Custom Alert state + const [alertInfo, setAlertInfo] = useState<{ + visible: boolean; + title: string; + message: string; + buttons: { text: string; onPress: () => void; style?: "cancel" | "destructive" | "default" }[]; + alertType: "info" | "success" | "error"; + }>({ + visible: false, + title: "", + message: "", + buttons: [], + alertType: "info" + }); + const initializeEditableData = (data: GroupDetails) => { setEditableData({ groupName: data.groupName, @@ -145,25 +141,24 @@ const EditGroupScreen: React.FC = ({ route }) => { latePaymentCharge: data.latePaymentCharge ?? 0, visibility: data.visibility, lateFeeMode: data.lateFeeMode ?? null, - lateFeeValue: data.lateFeeValue ?? null, + lateFeeValue: data.lateFeeValue ?? null }); }; - - const fetchGroupDetails = useCallback(async () => { setLoading(true); setError(null); try { - const response = await apiCall(`${BASE_URL}fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string }; - console.log("groupDetails", response) - - - if (response.status === 'success' && response.data) { + const response = (await apiCall(`${BASE_URL}fund/group/details?groupId=${groupId}`, "GET")) as { + status: string; + data?: GroupDetails; + message?: string; + }; + if (response.status === "success" && response.data) { setGroupDetails(response.data); initializeEditableData(response.data); } else { - throw new Error(response.message || 'Failed to fetch group data.'); + throw new Error(response.message || "Failed to fetch group data."); } } catch (e: any) { setError(e.message); @@ -173,164 +168,405 @@ const EditGroupScreen: React.FC = ({ route }) => { }, [groupId]); const getPaymentStatus = async () => { - const response = await apiCall(`${BASE_URL}payments/group/${groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number, allPaid: boolean, selectedUser: any }; message?: string }; - console.log("paymentStatus", response) - if (response.status === 'success') { + const response = (await apiCall(`${BASE_URL}payments/group/${groupId}/payment/paid`, "GET")) as { + status: string; + data?: { currentCycle: number; allPaid: boolean; selectedUser: any }; + message?: string; + }; + if (response.status === "success") { setPaymentStatus(response.data?.allPaid || false); setCurrentCycle(response.data?.currentCycle || null); - setSelectedUser(response.data) + setSelectedUser(response.data); } - } + }; - - console.log("groupid : ", groupId) useEffect(() => { - getPaymentStatus() - }, []) - + getPaymentStatus(); + }, []); useEffect(() => { fetchGroupDetails(); }, [fetchGroupDetails]); - - const handleCancel = () => { - if (groupDetails) { - initializeEditableData(groupDetails); - } + if (groupDetails) initializeEditableData(groupDetails); setEditingCard(null); }; - - const handleSave = async () => { if (!groupDetails) return; if (editableData.maxParticipants < groupDetails.totalParticipants) { - ToastAndroid.show(`Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`, ToastAndroid.SHORT); + setAlertInfo({ + visible: true, + title: "Invalid Input", + message: `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`, + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); return; } setSaving(true); try { - const res = await apiCall(`${BASE_URL}fund/groups/${groupDetails.id}/edit`, 'PUT', editableData); - console.log("Edit Group Response: ", res,editableData) - - Alert.alert('Success', 'Group has been updated.'); + await apiCall(`${BASE_URL}fund/groups/${groupDetails.id}/edit`, "PUT", editableData); + setAlertInfo({ + visible: true, + title: "Success", + message: "Group has been updated.", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "success" + }); setEditingCard(null); await fetchGroupDetails(); } catch (e: any) { - ToastAndroid.show(e.message || 'An unexpected error occurred.', ToastAndroid.SHORT); + setAlertInfo({ + visible: true, + title: "Error", + message: e.message || "An unexpected error occurred.", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); } finally { setSaving(false); } }; - - const handleDistribute = async () => { - navigation.navigate('RandomSelectUserScreen', { groupId, cycleNumber: currentCycle || 0 }); + navigation.navigate("RandomSelectUserScreen", { groupId, 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" }, + setAlertInfo({ + visible: true, + title: "Confirm Start", + message: "Are you sure you want to start this group? This action cannot be undone.", + alertType: "info", + buttons: [ + { text: "Cancel", onPress: () => setAlertInfo({ ...alertInfo, visible: false }), style: "cancel" }, { text: "Start", onPress: async () => { + setAlertInfo({ ...alertInfo, visible: false }); setStarting(true); try { - await apiCall(`${BASE_URL}fund/group/start?groupId=${groupDetails.id}`, 'GET'); - ToastAndroid.show('Group has been started successfully.', ToastAndroid.SHORT); + await apiCall(`${BASE_URL}fund/group/start?groupId=${groupDetails.id}`, "GET"); + setAlertInfo({ + visible: true, + title: "Success", + message: "Group has been started successfully.", + buttons: [ + { + text: "OK", + onPress: () => { + setAlertInfo({ ...alertInfo, visible: false }); + navigation.goBack(); + } + } + ], + alertType: "success" + }); await fetchGroupDetails(); - navigation.goBack() } catch (e: any) { - ToastAndroid.show(e.message || 'An unexpected error occurred.', ToastAndroid.SHORT); + setAlertInfo({ + visible: true, + title: "Error", + message: e.message || "An unexpected error occurred.", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); } finally { setStarting(false); } }, - style: 'destructive' + style: "destructive" } ] - ); + }); }; - const handleInvite = async () => { if (!groupDetails) return; setInviting(true); try { - const response = await apiCall(`${BASE_URL}invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string }; - + const response = (await apiCall(`${BASE_URL}invite/generate?groupId=${groupDetails.id}`, "GET")) as { data?: string }; const inviteLink = response.data; if (inviteLink) { Clipboard.setString(inviteLink); - ToastAndroid.show('Invitation link copied to clipboard.', ToastAndroid.SHORT); + setAlertInfo({ + visible: true, + title: "Link Copied", + message: "Invitation link copied to clipboard.", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "success" + }); await Share.share({ message: `Join my group on CircleCollect! ${inviteLink}`, url: inviteLink, - title: 'CircleCollect Group Invitation' + title: "CircleCollect Group Invitation" }); } else { - throw new Error('Could not generate invitation link.'); + throw new Error("Could not generate invitation link."); } } catch (error: any) { - ToastAndroid.show(error.message || 'Failed to generate invitation.', ToastAndroid.SHORT); + setAlertInfo({ + visible: true, + title: "Invitation Error", + message: error.message || "Failed to generate invitation.", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); } finally { setInviting(false); } }; - - const handleUpdateField = (field: keyof EditGroupPayload, value: any) => { setEditableData(prev => ({ ...prev, [field]: value })); }; + const pickImage = () => { + launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => { + if (response.didCancel) return; - // --- CORRECTED FUNCTION --- - // The 'isEditable' parameter was removed, and the condition now correctly - // uses 'isEditing' to show the input fields. - const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean) => { + if (response.errorCode) { + setAlertInfo({ + visible: true, + title: "Image Error", + message: response.errorMessage || "Something went wrong", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); + return; + } + + if (!response.assets || response.assets.length === 0) { + setAlertInfo({ + visible: true, + title: "No Image", + message: "No image selected", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); + return; + } + + const selectedImage: Asset = response.assets[0]; + const maxSizeKB = 1000; + const maxSizeBytes = maxSizeKB * 1024; + + if (selectedImage.fileSize && selectedImage.fileSize > maxSizeBytes) { + const currentSizeKB = (selectedImage.fileSize / 1024).toFixed(2); + setAlertInfo({ + visible: true, + title: "File Too Large", + message: `Selected image is ${currentSizeKB}KB. Please select an image smaller than ${maxSizeKB}KB.`, + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); + return; + } + + if (!selectedImage.uri) { + setAlertInfo({ + visible: true, + title: "Invalid File", + message: "Invalid image file", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); + return; + } + setSelectedImage(selectedImage.uri); + }); + }; + + const handleUpdateImage = async () => { + try { + const credentials = await Keychain.getGenericPassword(); + if (!credentials) { + setAlertInfo({ + visible: true, + title: "Not Logged In", + message: "You are not logged in.", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); + return; + } + const token = credentials.password; + + if (!selectedImage) { + setAlertInfo({ + visible: true, + title: "No Image", + message: "Please select an image first.", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); + return; + } + + setImageUpdating(true); + 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`; + const response = await fetch(url, { + method: "PUT", + headers: { + "Content-Type": "multipart/form-data", + "Authorization": `Bearer ${token}` + }, + body: formData as any + }); + + if (response?.status === 200) { + await fetchGroupDetails(); + setAlertInfo({ + visible: true, + title: "Success", + message: "Group image uploaded successfully.", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "success" + }); + } else { + throw new Error("Failed to upload image."); + } + + } catch (error: any) { + setAlertInfo({ + visible: true, + title: "Upload Error", + message: error.message || "Error updating group image.", + buttons: [{ text: "OK", onPress: () => setAlertInfo({ ...alertInfo, visible: false }) }], + alertType: "error" + }); + } 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"; + }; + + const renderField = ( + field: keyof EditGroupPayload, + label: string, + displayValue: string | number, + isEditing: 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': + case "groupName": return ( - handleUpdateField('cycleType', itemValue)} style={styles.picker}> - {['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => )} + 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': + case "customCycleDays": + if (editableData.cycleType !== "CUSTOM") return null; return ( - handleUpdateField('visibility', itemValue)} style={styles.picker}> + + handleUpdateField( + "customCycleDays", + text ? Number(text.replace(/[^0-9]/g, "")) : 0 + ) + } + keyboardType="numeric" + /> + ); + case "visibility": + return ( + handleUpdateField("visibility", itemValue)} + style={styles.picker} + > @@ -350,13 +586,15 @@ const EditGroupScreen: React.FC = ({ route }) => { )} ); - } + }; - - const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => { + const renderCard = ( + title: string, + fields: Array<{ field: keyof EditGroupPayload; label: string; displayValue: () => string | number }>, + cardKey: EditingCard + ) => { const isEditingThisCard = editingCard === cardKey; - // This isEditable check correctly controls the visibility of the "Update" button itself. - const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED'; + const isEditable = groupDetails?.status === "PENDING" || groupDetails?.status === "APPROVED"; return ( @@ -370,14 +608,13 @@ const EditGroupScreen: React.FC = ({ route }) => { {fields.map(({ field, label, displayValue }) => - // The call now correctly matches the updated renderField signature renderField(field, label, displayValue(), isEditingThisCard) )} {isEditingThisCard && ( - Cancel + Cancel {saving ? : Save} @@ -386,123 +623,24 @@ const EditGroupScreen: React.FC = ({ route }) => { )} ); - } + }; - - if (loading) return ; - if (error) return {error}; + if (loading) + return ( + + + + ); + if (error) + return ( + + {error} + + ); if (!groupDetails) return null; - - const isInvitable = groupDetails.status === 'APPROVED'; - const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants; - - - console.log(" myu status : ", groupDetails.status) - - const pickImage = () => { - launchImageLibrary({ mediaType: "photo", quality: 0.5 }, (response) => { - if (response.didCancel) return; - - if (response.errorCode) { - ToastAndroid.show(response.errorMessage || "Something went wrong", ToastAndroid.SHORT); - return; - } - - if (!response.assets || response.assets.length === 0) { - ToastAndroid.show("No image selected", ToastAndroid.SHORT); - return; - } - - const selectedImage = response.assets[0]; - const maxSizeKB = 1000; - const maxSizeBytes = maxSizeKB * 1024; - - if (selectedImage.fileSize && selectedImage.fileSize > maxSizeBytes) { - const currentSizeKB = (selectedImage.fileSize / 1024).toFixed(2); - Alert.alert( - "File Too Large", - `Selected image is ${currentSizeKB}KB. Please select an image smaller than ${maxSizeKB}KB.` - ); - return; - } - - if (!selectedImage.uri) { - ToastAndroid.show("Invalid image file", ToastAndroid.SHORT); - return; - } - setSelectedImage(selectedImage.uri); - }); - }; - - - const handleUpdateImage = async () => { - try { - const credentials = await Keychain.getGenericPassword(); - if (!credentials) { - ToastAndroid.show('You are not logged in', ToastAndroid.SHORT); - return; - } - const token = credentials.password; - - - if (!selectedImage) { - ToastAndroid.show("Please select an image first", ToastAndroid.SHORT); - return; - } - - - setImageUpdating(true); - - 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) - - 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); - } 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"; - }; + const isInvitable = groupDetails.status === "APPROVED"; + const canStartGroup = groupDetails.status === "APPROVED" && groupDetails.totalParticipants >= groupDetails.minParticipants; return ( @@ -515,23 +653,15 @@ const EditGroupScreen: React.FC = ({ route }) => { )} - - - - console.log("Error loading image")} - /> + + + + {}} /> - + = ({ route }) => { /> - {renderCard('Group Info', [ - { field: 'groupName', label: 'Group Name', displayValue: () => groupDetails.groupName }, - { field: 'description', label: 'Description', displayValue: () => groupDetails.description }, - ], 'groupInfo')} + {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" as keyof EditGroupPayload, + label: "Min Participants", + displayValue: () => `${groupDetails.minParticipants}` + }, + { + field: "latePaymentCharge", + label: "Late Fee", + displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString("en-IN")}` + } + ], + "financialRules" + )} - {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')} - + {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}) @@ -578,9 +754,13 @@ const EditGroupScreen: React.FC = ({ route }) => { groupDetails.participants.map(p => ( - - {`${p.firstName} ${p.lastName || ''}`.trim()} - {p.role === 'ORGANIZER' && Organizer} + + {`${p.firstName} ${p.lastName || ""}`.trim()} + {p.role === "ORGANIZER" && ( + + Organizer + + )} {p.mobile} @@ -592,91 +772,69 @@ const EditGroupScreen: React.FC = ({ route }) => { + {/* Fixed-Position Action Buttons */} {canStartGroup && ( - - {starting ? ( - - ) : ( - Activate Group - )} + + {starting ? : Activate Group} )} - {selectedUser?.cycleStatus !== "PENDING_PAYMENTS" && groupDetails.status === 'ACTIVE' && selectedUser?.cycleStatus === "PENDING_SELECTION" && ( - - - {starting ? ( - - ) : ( - Distribute - )} - - - ) - } - { - selectedUser?.selectedUser !== null && selectedUser?.cycleStatus === "READY_FOR_WITHDRAWAL" && - - setSelectedUserModel(true)} - /> - - } + {selectedUser?.cycleStatus !== "PENDING_PAYMENTS" && + groupDetails.status === "ACTIVE" && + selectedUser?.cycleStatus === "PENDING_SELECTION" && ( + + + {starting ? : Distribute} + + + )} - { - selectedUserModal && setSelectedUserModel(false)} /> - } + {selectedUser?.selectedUser != null && selectedUser?.cycleStatus === "READY_FOR_WITHDRAWAL" && ( + + setSelectedUserModel(true)} /> + + )} + + {selectedUserModal && setSelectedUserModel(false)} />} + + ); }; - - const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#F3F4F6' }, + 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' }, + centered: { flex: 1, justifyContent: "center", alignItems: "center" }, + errorText: { color: "#EF4444", fontSize: 16, textAlign: "center" }, headerContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", paddingHorizontal: 16, paddingTop: 16, paddingBottom: 8, borderBottomWidth: 1, - borderBottomColor: '#E5E7EB', - }, - - - - avatarContainer: { - position: "relative", - marginVertical: 16, + borderBottomColor: "#E5E7EB" }, + avatarContainer: { position: "relative", marginVertical: 16 }, avatar: { width: 120, height: 120, borderRadius: 60, borderWidth: 4, - borderColor: "#e2e8f0", + borderColor: "#e2e8f0" }, cameraIcon: { position: "absolute", - // right: 0, bottom: 0, backgroundColor: colors.pastelOrange, borderRadius: 20, @@ -686,45 +844,45 @@ const styles = StyleSheet.create({ alignItems: "center", borderWidth: 3, borderColor: "#fff", - elevation: 2, + 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 }, + 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', + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", paddingBottom: 10, - marginBottom: 12, + marginBottom: 12 }, - cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' }, + cardTitle: { fontSize: 18, fontWeight: "bold", color: "#1F2937" }, editButton: { padding: 8, backgroundColor: colors.pastelOrange, borderRadius: 15, elevation: 5 }, - editButtonText: { color: colors.black, fontSize: 15, 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' }, + editButtonText: { color: colors.black, fontSize: 15, 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' }, + cancelButton: { backgroundColor: "#E5E7EB" }, inviteButton: { backgroundColor: colors.pastelOrange }, - buttonText: { color: colors.black, fontSize: 15, fontWeight: '600' }, + buttonText: { color: colors.black, fontSize: 15, fontWeight: "600" }, floatingButtonContainer: { - position: 'absolute', + position: "absolute", bottom: 85, left: 20, right: 20, - alignItems: 'center', - zIndex: 1, + alignItems: "center", + zIndex: 1 }, activateButton: { backgroundColor: colors.pastelOrange, @@ -732,20 +890,18 @@ const styles = StyleSheet.create({ paddingVertical: 16, paddingHorizontal: 32, elevation: 8, - shadowColor: '#000', + shadowColor: "#000", shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 4, - width: '90%', - alignItems: 'center', + width: "90%", + alignItems: "center" }, activateButtonText: { - color: '#000000ff', + color: "#000000ff", fontSize: 18, - fontWeight: 'bold', - }, + fontWeight: "bold" + } }); - - export default EditGroupScreen;