Merge branch 'feature_pending' of https://git.zeptic.in/abintoppo_lincpay/CircleCollect into feature_profile

This commit is contained in:
Abin Toppo
2025-09-26 18:24:38 +05:30
5 changed files with 362 additions and 190 deletions
+109 -77
View File
@@ -8,11 +8,15 @@ import {
Platform, Platform,
TouchableOpacity, TouchableOpacity,
Alert, Alert,
Image,
} from "react-native"; } from "react-native";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import { CustomInput } from "../components/common/Custom"; import { CustomInput } from "../components/common/Custom";
import CustomButton from "../components/CustomButton"; import CustomButton from "../components/CustomButton";
import colors from "../components/colors"; 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() { export default function GroupCreationForm() {
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
@@ -31,6 +35,7 @@ export default function GroupCreationForm() {
}); });
const [errors, setErrors] = useState<any>({}); const [errors, setErrors] = useState<any>({});
const [selectedImage, setSelectedImage] = useState()
const handleChange = (key: string, value: string) => { const handleChange = (key: string, value: string) => {
setForm({ ...form, [key]: value }); setForm({ ...form, [key]: value });
@@ -60,12 +65,12 @@ export default function GroupCreationForm() {
const handleReview = () => { const handleReview = () => {
if (validateForm()) { if (validateForm()) {
const payload = { const payload = {
...form, ...form,
contributionAmount: Number(form.contributionAmount), contributionAmount: Number(form.contributionAmount),
customCycleDays: Number(form.customCycleDays) || 0, customCycleDays: Number(form.customCycleDays) || 0,
lateFeeValue: Number(form.lateFeeValue) || 0, lateFeeValue: Number(form.lateFeeValue) || 0,
maxParticipants: Number(form.maxParticipants), maxParticipants: Number(form.maxParticipants),
minParticipants: Number(form.minParticipants), minParticipants: Number(form.minParticipants),
}; };
console.log("Reviewing Payload:", payload); console.log("Reviewing Payload:", payload);
navigation.navigate("GroupReview", { form: payload }); navigation.navigate("GroupReview", { form: payload });
@@ -74,66 +79,68 @@ export default function GroupCreationForm() {
} }
}; };
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1, backgroundColor: "#f5f6fa", paddingTop: Platform.OS === "android" ? 40 : 0 }} style={{ flex: 1, backgroundColor: "#f5f6fa", paddingTop: Platform.OS === "android" ? 40 : 0 }}
behavior={Platform.OS === "ios" ? "padding" : "height"} behavior={Platform.OS === "ios" ? "padding" : "height"}
> >
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}> <ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
<View style={styles.titleContainer}><Text style={styles.title}>Create New Group</Text></View> <View style={styles.titleContainer}><Text style={styles.title}>Create New Group</Text></View>
<CustomInput label="Group Name" placeholder="e.g., 'Family Savings'" value={form.groupName} onChangeText={(val) => handleChange("groupName", val)} icon="people" error={errors.groupName} /> <CustomInput label="Group Name" placeholder="e.g., 'Family Savings'" value={form.groupName} onChangeText={(val) => handleChange("groupName", val)} icon="people" error={errors.groupName} />
<CustomInput label="Description" placeholder="e.g., 'Monthly fund for family needs'" value={form.description} onChangeText={(val) => handleChange("description", val)} multiline icon="document-text" error={errors.description} /> <CustomInput label="Description" placeholder="e.g., 'Monthly fund for family needs'" value={form.description} onChangeText={(val) => handleChange("description", val)} multiline icon="document-text" error={errors.description} />
<Section title="Group Settings"> <Section title="Group Settings">
<SettingRow label="Visibility"> <SettingRow label="Visibility">
<ToggleButtons <ToggleButtons
options={["PRIVATE", "PUBLIC"]} options={["PRIVATE", "PUBLIC"]}
selectedValue={form.visibility} selectedValue={form.visibility}
onSelect={(val) => handleChange("visibility", val)} onSelect={(val) => handleChange("visibility", val)}
/> />
</SettingRow> </SettingRow>
<SettingRow label="Fund Distribution"> <SettingRow label="Fund Distribution">
<ToggleButtons <ToggleButtons
options={distributionTypes} options={distributionTypes}
selectedValue={form.withdrawalMethod} selectedValue={form.withdrawalMethod}
onSelect={(val) => handleChange("withdrawalMethod", val)} onSelect={(val) => handleChange("withdrawalMethod", val)}
/> />
</SettingRow> </SettingRow>
</Section> </Section>
<Section title="Participant Rules"> <Section title="Participant Rules">
<View style={styles.participantsRow}> <View style={styles.participantsRow}>
<View style={styles.participantInput}><CustomInput label="Min" placeholder="e.g., 5" value={form.minParticipants} onChangeText={(val) => handleChange("minParticipants", val)} keyboardType="numeric" icon="person-add" error={errors.minParticipants} /></View> <View style={styles.participantInput}><CustomInput label="Min" placeholder="e.g., 5" value={form.minParticipants} onChangeText={(val) => handleChange("minParticipants", val)} keyboardType="numeric" icon="person-add" error={errors.minParticipants} /></View>
<View style={styles.participantInput}><CustomInput label="Max" placeholder="e.g., 10" value={form.maxParticipants} onChangeText={(val) => handleChange("maxParticipants", val)} keyboardType="numeric" icon="people-circle" error={errors.maxParticipants} /></View> <View style={styles.participantInput}><CustomInput label="Max" placeholder="e.g., 10" value={form.maxParticipants} onChangeText={(val) => handleChange("maxParticipants", val)} keyboardType="numeric" icon="people-circle" error={errors.maxParticipants} /></View>
</View> </View>
</Section> </Section>
<Section title="Financial Details"> <Section title="Financial Details">
<CustomInput label="Contribution Amount (INR)" placeholder="e.g., 5000" value={form.contributionAmount} onChangeText={(val) => handleChange("contributionAmount", val)} keyboardType="numeric" icon="cash" error={errors.contributionAmount} /> <CustomInput label="Contribution Amount (INR)" placeholder="e.g., 5000" value={form.contributionAmount} onChangeText={(val) => handleChange("contributionAmount", val)} keyboardType="numeric" icon="cash" error={errors.contributionAmount} />
<SettingRow label="Payment Cycle"> <SettingRow label="Payment Cycle">
<ToggleButtons <ToggleButtons
options={cycleTypes} options={cycleTypes}
selectedValue={form.cycleType} selectedValue={form.cycleType}
onSelect={(val) => handleChange("cycleType", val)} onSelect={(val) => handleChange("cycleType", val)}
isWrap={true} isWrap={true}
/> />
</SettingRow> </SettingRow>
{form.cycleType === "CUSTOM" && (<CustomInput label="Custom Cycle (Days)" placeholder="Enter number of days" value={form.customCycleDays} onChangeText={(val) => handleChange("customCycleDays", val)} keyboardType="numeric" icon="calendar" error={errors.customCycleDays} />)} {form.cycleType === "CUSTOM" && (<CustomInput label="Custom Cycle (Days)" placeholder="Enter number of days" value={form.customCycleDays} onChangeText={(val) => handleChange("customCycleDays", val)} keyboardType="numeric" icon="calendar" error={errors.customCycleDays} />)}
</Section> </Section>
<Section title="Late Fee Policy"> <Section title="Late Fee Policy">
<SettingRow label="Fee Mode"> <SettingRow label="Fee Mode">
<ToggleButtons <ToggleButtons
options={lateFeeModes} options={lateFeeModes}
selectedValue={form.lateFeeMode} selectedValue={form.lateFeeMode}
onSelect={(val) => handleChange("lateFeeMode", val)} onSelect={(val) => handleChange("lateFeeMode", val)}
/> />
</SettingRow> </SettingRow>
<CustomInput label="Late Fee Value (INR)" placeholder="e.g., 50" value={form.lateFeeValue} onChangeText={(val) => handleChange("lateFeeValue", val)} keyboardType="numeric" icon="alert-circle" error={errors.lateFeeValue} /> <CustomInput label="Late Fee Value (INR)" placeholder="e.g., 50" value={form.lateFeeValue} onChangeText={(val) => handleChange("lateFeeValue", val)} keyboardType="numeric" icon="alert-circle" error={errors.lateFeeValue} />
</Section> </Section>
<CustomButton title="Review Group" onPress={handleReview} style={{marginTop: 20}} /> <CustomButton title="Review Group" onPress={handleReview} style={{ marginTop: 20 }} />
</ScrollView> </ScrollView>
</KeyboardAvoidingView> </KeyboardAvoidingView>
); );
@@ -142,40 +149,40 @@ export default function GroupCreationForm() {
// --- Helper Components for a cleaner layout --- // --- Helper Components for a cleaner layout ---
const Section: React.FC<{ title: string, children: React.ReactNode }> = ({ title, children }) => ( const Section: React.FC<{ title: string, children: React.ReactNode }> = ({ title, children }) => (
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>{title}</Text> <Text style={styles.sectionTitle}>{title}</Text>
{children} {children}
</View> </View>
); );
const SettingRow: React.FC<{ label: string, children: React.ReactNode }> = ({ label, children }) => ( const SettingRow: React.FC<{ label: string, children: React.ReactNode }> = ({ label, children }) => (
<View style={styles.settingRow}> <View style={styles.settingRow}>
<Text style={styles.label}>{label}</Text> <Text style={styles.label}>{label}</Text>
{children} {children}
</View> </View>
); );
const ToggleButtons: React.FC<{ options: string[], selectedValue: string, onSelect: (value: string) => void, isWrap?: boolean }> = ({ options, selectedValue, onSelect, isWrap = false }) => ( const ToggleButtons: React.FC<{ options: string[], selectedValue: string, onSelect: (value: string) => void, isWrap?: boolean }> = ({ options, selectedValue, onSelect, isWrap = false }) => (
<View style={[styles.toggleContainer, isWrap && { flexWrap: 'wrap' }]}> <View style={[styles.toggleContainer, isWrap && { flexWrap: 'wrap' }]}>
{options.map((option) => ( {options.map((option) => (
<TouchableOpacity <TouchableOpacity
key={option} key={option}
onPress={() => onSelect(option)} onPress={() => onSelect(option)}
style={[styles.toggleButton, selectedValue === option && styles.selectedButton, isWrap && {flex: 0, paddingHorizontal: 15}]} style={[styles.toggleButton, selectedValue === option && styles.selectedButton, isWrap && { flex: 0, paddingHorizontal: 15 }]}
> >
<Text style={[styles.toggleButtonText, selectedValue === option && styles.selectedButtonText]}> <Text style={[styles.toggleButtonText, selectedValue === option && styles.selectedButtonText]}>
{option.charAt(0) + option.slice(1).toLowerCase()} {option.charAt(0) + option.slice(1).toLowerCase()}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
))} ))}
</View> </View>
); );
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flexGrow: 1, padding: 16, backgroundColor: "#f5f6fa" }, container: { flexGrow: 1, padding: 16, backgroundColor: "#f5f6fa" },
titleContainer: { marginBottom: 20, paddingHorizontal: 4 }, titleContainer: { marginBottom: 20, paddingHorizontal: 4 },
title: { fontSize: 28, fontWeight: "bold", color: "#1F2937" }, title: { fontSize: 28, fontWeight: "bold", color: "#1F2937" },
section: { section: {
backgroundColor: '#fff', backgroundColor: '#fff',
borderRadius: 12, borderRadius: 12,
@@ -196,26 +203,51 @@ const styles = StyleSheet.create({
paddingBottom: 10, 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: { settingRow: {
marginBottom: 16, marginBottom: 16,
}, },
label: { fontSize: 14, fontWeight: "600", marginBottom: 8, color: "#4B5563" }, label: { fontSize: 14, fontWeight: "600", marginBottom: 8, color: "#4B5563" },
toggleContainer: { flexDirection: "row", gap: 10 }, toggleContainer: { flexDirection: "row", gap: 10 },
toggleButton: { toggleButton: {
flex: 1, flex: 1,
paddingVertical: 12, paddingVertical: 12,
borderWidth: 1, borderWidth: 1,
borderColor: "#D1D5DB", borderColor: "#D1D5DB",
borderRadius: 8, borderRadius: 8,
alignItems: "center", alignItems: "center",
backgroundColor: "#F9FAFB" backgroundColor: "#F9FAFB"
}, },
selectedButton: { backgroundColor: colors.pastelGreen, borderColor: "#62b59a" }, selectedButton: { backgroundColor: colors.pastelGreen, borderColor: "#62b59a" },
toggleButtonText: { color: "#374151", fontWeight: "600", textTransform: 'capitalize' }, toggleButtonText: { color: "#374151", fontWeight: "600", textTransform: 'capitalize' },
selectedButtonText: { color: "#1F2937" }, selectedButtonText: { color: "#1F2937" },
participantsRow: { flexDirection: "row", gap: 15 }, participantsRow: { flexDirection: "row", gap: 15 },
participantInput: { flex: 1 }, participantInput: { flex: 1 },
}); });
+247 -110
View File
@@ -10,6 +10,8 @@ import {
Alert, Alert,
ActivityIndicator, ActivityIndicator,
Share, Share,
Image,
ToastAndroid,
} from 'react-native'; } from 'react-native';
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
import { apiCall } from '../../utils/ApiHelper'; // 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 { CompositeScreenProps, useNavigation } from '@react-navigation/native';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs'; import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path 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'; import { StackNavigationProp } from '@react-navigation/stack';
@@ -54,6 +61,7 @@ export interface GroupDetails {
organizerId: string; organizerId: string;
totalParticipants: number; totalParticipants: number;
participants: Participant[]; participants: Participant[];
imageBlob:string
} }
@@ -100,7 +108,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
lateFeeMode: null, lateFeeMode: null,
lateFeeValue: null, lateFeeValue: null,
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [starting, setStarting] = useState(false); const [starting, setStarting] = useState(false);
@@ -108,6 +116,8 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [paymentStatus, setPaymentStatus] = useState<boolean | null>(null); const [paymentStatus, setPaymentStatus] = useState<boolean | null>(null);
const [currentCycle, setCurrentCycle] = useState<number | null>(null); const [currentCycle, setCurrentCycle] = useState<number | null>(null);
const [selectedImage, setSelectedImage] = useState<string | null>()
const [imageUpdating, setImageUpdating] = useState<boolean>(false)
const initializeEditableData = (data: GroupDetails) => { const initializeEditableData = (data: GroupDetails) => {
setEditableData({ setEditableData({
@@ -130,7 +140,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setError(null); setError(null);
try { 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 }; 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) { if (response.status === 'success' && response.data) {
setGroupDetails(response.data); setGroupDetails(response.data);
initializeEditableData(response.data); initializeEditableData(response.data);
@@ -161,7 +171,7 @@ const getPaymentStatus = async()=>{
const handleCancel = () => { const handleCancel = () => {
if (groupDetails) { if (groupDetails) {
initializeEditableData(groupDetails); initializeEditableData(groupDetails);
} }
setEditingCard(null); setEditingCard(null);
}; };
@@ -172,10 +182,10 @@ const getPaymentStatus = async()=>{
if (editableData.maxParticipants < groupDetails.totalParticipants) { if (editableData.maxParticipants < groupDetails.totalParticipants) {
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`); Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
return; return;
} }
setSaving(true); setSaving(true);
try { try {
await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/groups/${groupDetails.id}/edit`, 'PUT', editableData); 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 () => { const handleInvite = async () => {
if (!groupDetails) return; if (!groupDetails) return;
setInviting(true); 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 response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
const inviteLink = response.data; const inviteLink = response.data;
if (inviteLink) { if (inviteLink) {
Clipboard.setString(inviteLink); Clipboard.setString(inviteLink);
Alert.alert('Copied!', 'Invitation link copied to clipboard.'); Alert.alert('Copied!', 'Invitation link copied to clipboard.');
await Share.share({ await Share.share({
message: `Join my group on CircleCollect! ${inviteLink}`, message: `Join my group on CircleCollect! ${inviteLink}`,
url: inviteLink, url: inviteLink,
title: 'CircleCollect Group Invitation' title: 'CircleCollect Group Invitation'
}); });
} else { } else {
throw new Error('Could not generate invitation link.'); throw new Error('Could not generate invitation link.');
} }
} catch (error: any) { } catch (error: any) {
Alert.alert('Error', error.message || 'Failed to generate invitation.'); Alert.alert('Error', error.message || 'Failed to generate invitation.');
} finally { } 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 renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean, isEditable: boolean) => {
const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" }; const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
const getInputForField = () => { const getInputForField = () => {
switch(field) { switch (field) {
case 'groupName': case 'groupName':
return <TextInput {...commonInputProps} value={editableData.groupName} onChangeText={(text) => handleUpdateField('groupName', text)} />; return <TextInput {...commonInputProps} value={editableData.groupName} onChangeText={(text) => handleUpdateField('groupName', text)} />;
case 'description': case 'description':
return <TextInput {...commonInputProps} style={[styles.input, styles.textArea]} value={editableData.description} onChangeText={(text) => handleUpdateField('description', text)} multiline />; return <TextInput {...commonInputProps} style={[styles.input, styles.textArea]} value={editableData.description} onChangeText={(text) => handleUpdateField('description', text)} multiline />;
case 'contributionAmount': case 'contributionAmount':
return <TextInput {...commonInputProps} value={String(editableData.contributionAmount ?? '')} onChangeText={(text) => handleUpdateField('contributionAmount', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; return <TextInput {...commonInputProps} value={String(editableData.contributionAmount ?? '')} onChangeText={(text) => handleUpdateField('contributionAmount', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
case 'maxParticipants': case 'maxParticipants':
return <TextInput {...commonInputProps} value={String(editableData.maxParticipants ?? '')} onChangeText={(text) => handleUpdateField('maxParticipants', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; return <TextInput {...commonInputProps} value={String(editableData.maxParticipants ?? '')} onChangeText={(text) => handleUpdateField('maxParticipants', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
case 'latePaymentCharge': case 'latePaymentCharge':
return <TextInput {...commonInputProps} value={String(editableData.latePaymentCharge ?? '')} onChangeText={(text) => handleUpdateField('latePaymentCharge', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; return <TextInput {...commonInputProps} value={String(editableData.latePaymentCharge ?? '')} onChangeText={(text) => handleUpdateField('latePaymentCharge', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
case 'cycleType': case 'cycleType':
return ( return (
<Picker selectedValue={editableData.cycleType} onValueChange={(itemValue) => handleUpdateField('cycleType', itemValue)} style={styles.picker}> <Picker selectedValue={editableData.cycleType} onValueChange={(itemValue) => handleUpdateField('cycleType', itemValue)} style={styles.picker}>
{['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => <Picker.Item key={c} label={c} value={c} />)} {['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => <Picker.Item key={c} label={c} value={c} />)}
</Picker> </Picker>
); );
case 'customCycleDays': case 'customCycleDays':
if (editableData.cycleType !== 'CUSTOM') return null; if (editableData.cycleType !== 'CUSTOM') return null;
return <TextInput {...commonInputProps} value={String(editableData.customCycleDays ?? '')} onChangeText={(text) => handleUpdateField('customCycleDays', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; return <TextInput {...commonInputProps} value={String(editableData.customCycleDays ?? '')} onChangeText={(text) => handleUpdateField('customCycleDays', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
case 'visibility': case 'visibility':
return ( return (
<Picker selectedValue={editableData.visibility} onValueChange={(itemValue) => handleUpdateField('visibility', itemValue)} style={styles.picker}> <Picker selectedValue={editableData.visibility} onValueChange={(itemValue) => handleUpdateField('visibility', itemValue)} style={styles.picker}>
<Picker.Item label="PUBLIC" value="PUBLIC" /> <Picker.Item label="PUBLIC" value="PUBLIC" />
<Picker.Item label="PRIVATE" value="PRIVATE" /> <Picker.Item label="PRIVATE" value="PRIVATE" />
</Picker> </Picker>
); );
default: default:
return null; return null;
} }
}; };
return ( return (
<View style={styles.fieldContainer}> <View style={styles.fieldContainer}>
<Text style={styles.infoLabel}>{label}</Text> <Text style={styles.infoLabel}>{label}</Text>
{isEditing && isEditable ? ( {isEditing && isEditable ? (
<View style={{marginTop: 8}}>{getInputForField()}</View> <View style={{ marginTop: 8 }}>{getInputForField()}</View>
) : ( ) : (
<Text style={styles.infoValue}>{displayValue}</Text> <Text style={styles.infoValue}>{displayValue}</Text>
)} )}
</View> </View>
); );
} }
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; const isEditingThisCard = editingCard === cardKey;
return ( return (
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<Text style={styles.cardTitle}>{title}</Text> <Text style={styles.cardTitle}>{title}</Text>
{isEditable && !editingCard && ( {isEditable && !editingCard && (
<TouchableOpacity onPress={() => setEditingCard(cardKey)} style={styles.editButton}> <TouchableOpacity onPress={() => setEditingCard(cardKey)} style={styles.editButton}>
<Text style={styles.editButtonText}>✎</Text> <Text style={styles.editButtonText}>✎</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
</View>
{fields.map(({ field, label, displayValue }) =>
renderField(field, label, displayValue(), isEditingThisCard, isEditable)
)}
{isEditingThisCard && (
<View style={styles.cardActions}>
<TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={handleCancel} disabled={saving}>
<Text style={[styles.buttonText, { color: '#374151' }]}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, styles.saveButton]} onPress={handleSave} disabled={saving}>
{saving ? <ActivityIndicator color="#FFFFFF" size="small" /> : <Text style={styles.buttonText}>Save</Text>}
</TouchableOpacity>
</View>
)}
</View> </View>
{fields.map(({ field, label, displayValue }) =>
renderField(field, label, displayValue(), isEditingThisCard)
)}
{isEditingThisCard && (
<View style={styles.cardActions}>
<TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={handleCancel} disabled={saving}>
<Text style={[styles.buttonText, { color: '#374151' }]}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.button, styles.saveButton]} onPress={handleSave} disabled={saving}>
{saving ? <ActivityIndicator color="#FFFFFF" size="small" /> : <Text style={styles.buttonText}>Save</Text>}
</TouchableOpacity>
</View>
)}
</View>
); );
} }
@@ -344,6 +354,84 @@ const getPaymentStatus = async()=>{
const isInvitable = groupDetails.status === 'APPROVED'; const isInvitable = groupDetails.status === 'APPROVED';
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants; 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 ( return (
<AppView style={{ flex: 1, paddingHorizontal: 0 }}> <AppView style={{ flex: 1, paddingHorizontal: 0 }}>
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
@@ -361,9 +449,31 @@ const getPaymentStatus = async()=>{
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
> >
<View style={[styles.avatarContainer, { flexDirection: 'row', flex: 1, gap: 10 }]}>
<View style={{}}>
<Image
// source={{ uri: groupDetails?.imageBlob ? groupDetails?.imageBlob : selectedImage || "https://img.freepik.com/premium-vector/people-group-frame-icon_791764-2649.jpg" }}
source={{ uri: getProfileImage() }}
style={styles.avatar}
onError={() => console.log("Error loading image")}
/>
<TouchableOpacity style={styles.cameraIcon} onPress={pickImage}>
<Ionicons name="camera" size={18} color="#000" />
</TouchableOpacity>
</View>
<View style={{justifyContent:'center'}}>
<CustomButton
title={imageUpdating ? "Updating..." : "Update Image"}
onPress={handleUpdateImage}
icon={imageUpdating ? undefined : "cloud-upload"}
style={{paddingHorizontal:20}}
buttonBackground={colors.pastelBlue}
/>
</View>
</View>
{renderCard('Group Info', [ {renderCard('Group Info', [
{ field: 'groupName', label: 'Group Name', displayValue: () => groupDetails.groupName }, { field: 'groupName', label: 'Group Name', displayValue: () => groupDetails.groupName },
{ field: 'description', label: 'Description', displayValue: () => groupDetails.description }, { field: 'description', label: 'Description', displayValue: () => groupDetails.description },
], 'groupInfo')} ], 'groupInfo')}
@@ -384,28 +494,28 @@ const getPaymentStatus = async()=>{
{renderCard('Configuration', [ {renderCard('Configuration', [
{ field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType }, { field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType },
...(groupDetails.cycleType === 'CUSTOM' ? [{ field: 'customCycleDays' as keyof EditGroupPayload, label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays} days` }] : []), ...(groupDetails.cycleType === 'CUSTOM' ? [{ field: 'customCycleDays' as keyof EditGroupPayload, label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays} days` }] : []),
{ field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility }, { field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility },
], 'configuration')} ], 'configuration')}
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text> <Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
{groupDetails.participants.length > 0 ? ( {groupDetails.participants.length > 0 ? (
groupDetails.participants.map(p => ( groupDetails.participants.map(p => (
<View key={p.id} style={styles.participantRow}> <View key={p.id} style={styles.participantRow}>
<View> <View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}> <View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={styles.participantName}>{`${p.firstName} ${p.lastName || ''}`.trim()}</Text> <Text style={styles.participantName}>{`${p.firstName} ${p.lastName || ''}`.trim()}</Text>
{p.role === 'ORGANIZER' && <View style={styles.organizerBadge}><Text style={styles.organizerBadgeText}>Organizer</Text></View>} {p.role === 'ORGANIZER' && <View style={styles.organizerBadge}><Text style={styles.organizerBadgeText}>Organizer</Text></View>}
</View> </View>
<Text style={styles.participantMobile}>{p.mobile}</Text> <Text style={styles.participantMobile}>{p.mobile}</Text>
</View> </View>
</View> </View>
)) ))
) : ( ) : (
<Text style={styles.noParticipantsText}>No participants yet.</Text> <Text style={styles.noParticipantsText}>No participants yet.</Text>
)} )}
</View> </View>
</ScrollView> </ScrollView>
@@ -447,7 +557,7 @@ const getPaymentStatus = async()=>{
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' }, 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' }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' }, errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
headerContainer: { headerContainer: {
@@ -460,6 +570,33 @@ const styles = StyleSheet.create({
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#E5E7EB', 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' }, headerTitle: { fontSize: 28, fontWeight: 'bold', color: '#111827' },
card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 }, card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 },
cardHeader: { cardHeader: {
@@ -25,6 +25,8 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
{state.routes.map((route, index) => { {state.routes.map((route, index) => {
const { options } = descriptors[route.key]; const { options } = descriptors[route.key];
const label = options.tabBarLabel ?? options.title ?? route.name; const label = options.tabBarLabel ?? options.title ?? route.name;
console.log("option : ", options)
// A custom option to pass the icon name // A custom option to pass the icon name
const iconName = options.tabBarIconName ?? 'help-circle'; const iconName = options.tabBarIconName ?? 'help-circle';
@@ -66,7 +68,7 @@ const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) =
color={isFocused ? '#4F46E5' : '#6B7280'} color={isFocused ? '#4F46E5' : '#6B7280'}
/> />
<Text style={[styles.tabLabel, { color: isFocused ? '#4F46E5' : '#6B7280' }]}> <Text style={[styles.tabLabel, { color: isFocused ? '#4F46E5' : '#6B7280' }]}>
{label} {label as string}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
); );
@@ -118,6 +120,7 @@ const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
tabBarLabel: 'Members', tabBarLabel: 'Members',
}} }}
/> />
{/* You can easily add more tabs here in the future */} {/* You can easily add more tabs here in the future */}
</Tab.Navigator> </Tab.Navigator>
-2
View File
@@ -445,8 +445,6 @@ const Profile = () => {
icon={imageUpdating ? undefined : "cloud-upload"} icon={imageUpdating ? undefined : "cloud-upload"}
style={styles.updateImageButton} style={styles.updateImageButton}
buttonBackground={colors.pastelBlue} buttonBackground={colors.pastelBlue}
/> />
</View> </View>
+2
View File
@@ -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 UPDATE_BANK_URL = `${BASE_URL}user/bank/update`;
export const CONTRIBUTE = `${BASE_URL}payments/groups/`; export const CONTRIBUTE = `${BASE_URL}payments/groups/`;
export const TRANSACTION = `${BASE_URL}payments/my-transactions`; export const TRANSACTION = `${BASE_URL}payments/my-transactions`;
export const UPLOAD_GROUP_IMAGE = `${BASE_URL}fund/groups/`;