diff --git a/src/screens/GroupCreation.tsx b/src/screens/GroupCreation.tsx index 050ae64..16d1745 100644 --- a/src/screens/GroupCreation.tsx +++ b/src/screens/GroupCreation.tsx @@ -8,11 +8,15 @@ import { Platform, TouchableOpacity, Alert, + Image, } from "react-native"; import { useNavigation } from "@react-navigation/native"; import { CustomInput } from "../components/common/Custom"; import CustomButton from "../components/CustomButton"; import colors from "../components/colors"; +import { launchImageLibrary } from "react-native-image-picker"; +import Ionicons from "@react-native-vector-icons/ionicons"; +import { UPLOAD_GROUP_IMAGE } from "../utils/ApiUrls"; export default function GroupCreationForm() { const navigation = useNavigation(); @@ -31,6 +35,7 @@ export default function GroupCreationForm() { }); const [errors, setErrors] = useState({}); + const [selectedImage, setSelectedImage] = useState() const handleChange = (key: string, value: string) => { setForm({ ...form, [key]: value }); @@ -60,12 +65,12 @@ export default function GroupCreationForm() { const handleReview = () => { if (validateForm()) { const payload = { - ...form, - contributionAmount: Number(form.contributionAmount), - customCycleDays: Number(form.customCycleDays) || 0, - lateFeeValue: Number(form.lateFeeValue) || 0, - maxParticipants: Number(form.maxParticipants), - minParticipants: Number(form.minParticipants), + ...form, + contributionAmount: Number(form.contributionAmount), + customCycleDays: Number(form.customCycleDays) || 0, + lateFeeValue: Number(form.lateFeeValue) || 0, + maxParticipants: Number(form.maxParticipants), + minParticipants: Number(form.minParticipants), }; console.log("Reviewing Payload:", payload); navigation.navigate("GroupReview", { form: payload }); @@ -74,66 +79,68 @@ export default function GroupCreationForm() { } }; + return ( + Create New Group - + handleChange("groupName", val)} icon="people" error={errors.groupName} /> handleChange("description", val)} multiline icon="document-text" error={errors.description} /> - +
- - handleChange("visibility", val)} - /> - - - handleChange("withdrawalMethod", val)} - /> - + + handleChange("visibility", val)} + /> + + + handleChange("withdrawalMethod", val)} + /> +
- handleChange("minParticipants", val)} keyboardType="numeric" icon="person-add" error={errors.minParticipants} /> - handleChange("maxParticipants", val)} keyboardType="numeric" icon="people-circle" error={errors.maxParticipants} /> + handleChange("minParticipants", val)} keyboardType="numeric" icon="person-add" error={errors.minParticipants} /> + handleChange("maxParticipants", val)} keyboardType="numeric" icon="people-circle" error={errors.maxParticipants} />
- +
- handleChange("contributionAmount", val)} keyboardType="numeric" icon="cash" error={errors.contributionAmount} /> - - handleChange("cycleType", val)} - isWrap={true} - /> - - {form.cycleType === "CUSTOM" && ( handleChange("customCycleDays", val)} keyboardType="numeric" icon="calendar" error={errors.customCycleDays} />)} + handleChange("contributionAmount", val)} keyboardType="numeric" icon="cash" error={errors.contributionAmount} /> + + handleChange("cycleType", val)} + isWrap={true} + /> + + {form.cycleType === "CUSTOM" && ( handleChange("customCycleDays", val)} keyboardType="numeric" icon="calendar" error={errors.customCycleDays} />)}
- - handleChange("lateFeeMode", val)} - /> - - handleChange("lateFeeValue", val)} keyboardType="numeric" icon="alert-circle" error={errors.lateFeeValue} /> + + handleChange("lateFeeMode", val)} + /> + + handleChange("lateFeeValue", val)} keyboardType="numeric" icon="alert-circle" error={errors.lateFeeValue} />
- +
); @@ -142,40 +149,40 @@ export default function GroupCreationForm() { // --- Helper Components for a cleaner layout --- const Section: React.FC<{ title: string, children: React.ReactNode }> = ({ title, children }) => ( - - {title} - {children} - + + {title} + {children} + ); const SettingRow: React.FC<{ label: string, children: React.ReactNode }> = ({ label, children }) => ( - - {label} - {children} - + + {label} + {children} + ); const ToggleButtons: React.FC<{ options: string[], selectedValue: string, onSelect: (value: string) => void, isWrap?: boolean }> = ({ options, selectedValue, onSelect, isWrap = false }) => ( - - {options.map((option) => ( - onSelect(option)} - style={[styles.toggleButton, selectedValue === option && styles.selectedButton, isWrap && {flex: 0, paddingHorizontal: 15}]} - > - - {option.charAt(0) + option.slice(1).toLowerCase()} - - - ))} - + + {options.map((option) => ( + onSelect(option)} + style={[styles.toggleButton, selectedValue === option && styles.selectedButton, isWrap && { flex: 0, paddingHorizontal: 15 }]} + > + + {option.charAt(0) + option.slice(1).toLowerCase()} + + + ))} + ); const styles = StyleSheet.create({ container: { flexGrow: 1, padding: 16, backgroundColor: "#f5f6fa" }, titleContainer: { marginBottom: 20, paddingHorizontal: 4 }, title: { fontSize: 28, fontWeight: "bold", color: "#1F2937" }, - + section: { backgroundColor: '#fff', borderRadius: 12, @@ -196,26 +203,51 @@ const styles = StyleSheet.create({ paddingBottom: 10, }, + avatarContainer: { + position: "relative", + marginBottom: 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, + }, settingRow: { marginBottom: 16, }, - + label: { fontSize: 14, fontWeight: "600", marginBottom: 8, color: "#4B5563" }, - + toggleContainer: { flexDirection: "row", gap: 10 }, - toggleButton: { - flex: 1, - paddingVertical: 12, - borderWidth: 1, - borderColor: "#D1D5DB", - borderRadius: 8, - alignItems: "center", - backgroundColor: "#F9FAFB" + toggleButton: { + flex: 1, + paddingVertical: 12, + borderWidth: 1, + borderColor: "#D1D5DB", + borderRadius: 8, + alignItems: "center", + backgroundColor: "#F9FAFB" }, selectedButton: { backgroundColor: colors.pastelGreen, borderColor: "#62b59a" }, toggleButtonText: { color: "#374151", fontWeight: "600", textTransform: 'capitalize' }, selectedButtonText: { color: "#1F2937" }, - + participantsRow: { flexDirection: "row", gap: 15 }, participantInput: { flex: 1 }, }); diff --git a/src/screens/management/EditGroupScreen.tsx b/src/screens/management/EditGroupScreen.tsx index c9cf523..2f656f4 100644 --- a/src/screens/management/EditGroupScreen.tsx +++ b/src/screens/management/EditGroupScreen.tsx @@ -10,6 +10,8 @@ import { Alert, ActivityIndicator, Share, + Image, + ToastAndroid, } from 'react-native'; import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path import { apiCall } from '../../utils/ApiHelper'; // Adjust path @@ -20,6 +22,11 @@ import Clipboard from '@react-native-clipboard/clipboard'; import { CompositeScreenProps, useNavigation } from '@react-navigation/native'; import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; import { ManagementTabParamList } from './ManagementTabNavigation'; // 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'; @@ -54,6 +61,7 @@ export interface GroupDetails { organizerId: string; totalParticipants: number; participants: Participant[]; + imageBlob:string } @@ -100,7 +108,7 @@ const EditGroupScreen: React.FC = ({ route }) => { lateFeeMode: null, lateFeeValue: null, }); - + const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [starting, setStarting] = useState(false); @@ -108,6 +116,8 @@ const EditGroupScreen: React.FC = ({ route }) => { 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({ @@ -130,7 +140,7 @@ const EditGroupScreen: React.FC = ({ route }) => { 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) + console.log("groupDetails", response) if (response.status === 'success' && response.data) { setGroupDetails(response.data); initializeEditableData(response.data); @@ -161,7 +171,7 @@ const getPaymentStatus = async()=>{ const handleCancel = () => { if (groupDetails) { - initializeEditableData(groupDetails); + initializeEditableData(groupDetails); } setEditingCard(null); }; @@ -172,10 +182,10 @@ const getPaymentStatus = async()=>{ if (editableData.maxParticipants < groupDetails.totalParticipants) { - Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`); - return; + 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); @@ -220,7 +230,7 @@ const getPaymentStatus = async()=>{ ] ); }; - + const handleInvite = async () => { if (!groupDetails) return; setInviting(true); @@ -228,23 +238,23 @@ const getPaymentStatus = async()=>{ 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.'); - } + 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.'); + Alert.alert('Error', error.message || 'Failed to generate invitation.'); } finally { - setInviting(false); + setInviting(false); } }; @@ -256,82 +266,82 @@ const getPaymentStatus = async()=>{ 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; - } + 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} - )} - + + {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, isEditable) - )} - - {isEditingThisCard && ( - - - Cancel - - - {saving ? : Save} - - - )} + + + {title} + {isEditable && !editingCard && ( + setEditingCard(cardKey)} style={styles.editButton}> + ✎ + + )} + + {fields.map(({ field, label, displayValue }) => + renderField(field, label, displayValue(), isEditingThisCard) + )} + + {isEditingThisCard && ( + + + Cancel + + + {saving ? : Save} + + + )} + ); } @@ -344,6 +354,84 @@ const getPaymentStatus = async()=>{ 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 ( @@ -361,9 +449,31 @@ const getPaymentStatus = async()=>{ contentContainerStyle={styles.contentContainer} showsVerticalScrollIndicator={false} > + + + console.log("Error loading image")} + /> + + + + + + + + {renderCard('Group Info', [ - { field: 'groupName', label: 'Group Name', displayValue: () => groupDetails.groupName }, - { field: 'description', label: 'Description', displayValue: () => groupDetails.description }, + { field: 'groupName', label: 'Group Name', displayValue: () => groupDetails.groupName }, + { field: 'description', label: 'Description', displayValue: () => groupDetails.description }, ], 'groupInfo')} @@ -384,28 +494,28 @@ const getPaymentStatus = async()=>{ {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 }, + { 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. - )} + 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. + )} @@ -447,7 +557,7 @@ const getPaymentStatus = async()=>{ const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F3F4F6' }, - contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 }, + contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' }, headerContainer: { @@ -460,6 +570,33 @@ const styles = StyleSheet.create({ 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: { diff --git a/src/screens/management/ManagementTabNavigation.tsx b/src/screens/management/ManagementTabNavigation.tsx index 239fbe3..dca74ad 100644 --- a/src/screens/management/ManagementTabNavigation.tsx +++ b/src/screens/management/ManagementTabNavigation.tsx @@ -25,6 +25,8 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) = {state.routes.map((route, index) => { const { options } = descriptors[route.key]; const label = options.tabBarLabel ?? options.title ?? route.name; + + console.log("option : ", options) // A custom option to pass the icon name const iconName = options.tabBarIconName ?? 'help-circle'; @@ -66,7 +68,7 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) = color={isFocused ? '#4F46E5' : '#6B7280'} /> - {label} + {label as string} ); @@ -118,6 +120,7 @@ const ManagementTabNavigation: React.FC = ({ route }) => { tabBarLabel: 'Members', }} + /> {/* You can easily add more tabs here in the future */} diff --git a/src/screens/profile/Profile.tsx b/src/screens/profile/Profile.tsx index 85f980c..0400b69 100644 --- a/src/screens/profile/Profile.tsx +++ b/src/screens/profile/Profile.tsx @@ -445,8 +445,6 @@ const Profile = () => { icon={imageUpdating ? undefined : "cloud-upload"} style={styles.updateImageButton} buttonBackground={colors.pastelBlue} - - /> diff --git a/src/utils/ApiUrls.tsx b/src/utils/ApiUrls.tsx index dc86833..23f8052 100644 --- a/src/utils/ApiUrls.tsx +++ b/src/utils/ApiUrls.tsx @@ -20,3 +20,5 @@ export const GROUP_ALL_MEMBER = `${BASE_URL}fund/group/all/member` export const UPDATE_BANK_URL = `${BASE_URL}user/bank/update`; export const CONTRIBUTE = `${BASE_URL}payments/groups/`; export const TRANSACTION = `${BASE_URL}payments/my-transactions`; +export const UPLOAD_GROUP_IMAGE = `${BASE_URL}fund/groups/`; +