manage groups

This commit is contained in:
Abin Toppo
2025-09-22 11:00:42 +05:30
parent b9e5095c9b
commit 5a9e80f56f
4 changed files with 502 additions and 223 deletions
+187 -22
View File
@@ -2,42 +2,207 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet } from 'react-native';
import { JoinedGroup } from '../types';
// Update the type to match the full data structure
interface JoinedGroup {
id: string;
groupName: string;
description: string;
visibility: 'PUBLIC' | 'PRIVATE';
createdByUserId: number;
createdByName: string; // Renamed from createdBy for clarity
startDate: [number, number, number] | null;
endDate: [number, number, number] | null;
minParticipants: number;
maxParticipants: number;
currentParticipants: number;
contributionAmount: number;
cycleType: 'MONTHLY' | 'WEEKLY' | 'QUARTERLY' | 'CUSTOM';
status: 'ACTIVE' | 'PENDING' | 'COMPLETED';
}
interface JoinedGroupCardProps { interface JoinedGroupCardProps {
item: JoinedGroup; item: JoinedGroup;
} }
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }) => ( // Helper function to format dates
<View style={styles.card}> const formatDate = (dateArray: [number, number, number] | null): string => {
<View> if (!dateArray) return 'N/A';
<Text style={styles.cardTitle}>{item.groupName}</Text> const date = new Date(dateArray[0], dateArray[1] - 1, dateArray[2]);
<Text style={styles.cardSubtitle}>Created by {item.createdBy}</Text> return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
};
// Helper to determine status color
const getStatusStyle = (status: JoinedGroup['status']) => {
switch (status) {
case 'ACTIVE':
return {
backgroundColor: '#D1FAE5', // Green tint
textColor: '#065F46', // Dark Green
};
case 'PENDING':
return {
backgroundColor: '#FEF3C7', // Yellow tint
textColor: '#92400E', // Dark Yellow/Brown
};
case 'COMPLETED':
return {
backgroundColor: '#E5E7EB', // Gray tint
textColor: '#4B5563', // Dark Gray
};
default:
return {
backgroundColor: '#E5E7EB',
textColor: '#4B5563',
};
}
};
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }) => {
const progress = (item.currentParticipants / item.maxParticipants) * 100;
const statusStyle = getStatusStyle(item.status);
return (
<View style={styles.card}>
{/* Header */}
<View style={styles.header}>
<Text style={styles.groupName}>{item.groupName}</Text>
<View style={[styles.statusBadge, { backgroundColor: statusStyle.backgroundColor }]}>
<Text style={[styles.statusText, { color: statusStyle.textColor }]}>{item.status}</Text>
</View>
</View>
<Text style={styles.createdBy}>Created by {item.createdByName}</Text>
{/* Financial Info */}
<View style={styles.infoRow}>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Contribution</Text>
<Text style={styles.infoValue}>₹{item.contributionAmount.toLocaleString('en-IN')}</Text>
</View>
<View style={styles.infoItem}>
<Text style={styles.infoLabel}>Cycle</Text>
<Text style={styles.infoValue}>{item.cycleType}</Text>
</View>
</View>
{/* Participants Progress */}
<View style={styles.progressContainer}>
<View style={styles.progressLabels}>
<Text style={styles.participantText}>Participants</Text>
<Text style={styles.participantCount}>{item.currentParticipants} / {item.maxParticipants}</Text>
</View>
<View style={styles.progressBarBackground}>
<View style={[styles.progressBarFill, { width: `${progress}%` }]} />
</View>
</View>
{/* Dates */}
<View style={styles.footer}>
<Text style={styles.dateText}>
{formatDate(item.startDate)} - {formatDate(item.endDate)}
</Text>
</View>
</View> </View>
</View> );
); };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
backgroundColor: '#fff', backgroundColor: '#FFFFFF',
borderRadius: 12, borderRadius: 16,
padding: 16, padding: 20,
marginBottom: 12, marginBottom: 16,
shadowColor: '#374151',
shadowOpacity: 0.08,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 3,
},
header: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
elevation: 2, alignItems: 'flex-start',
shadowColor: '#000', marginBottom: 4,
shadowOpacity: 0.05,
shadowRadius: 4,
}, },
cardTitle: { groupName: {
fontSize: 17, fontSize: 20,
fontWeight: 'bold', fontWeight: 'bold',
color: '#343a40', color: '#1F2937',
flexShrink: 1,
}, },
cardSubtitle: { createdBy: {
fontSize: 14, fontSize: 14,
color: '#6c757d', color: '#6B7280',
marginBottom: 16,
},
statusBadge: {
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 12,
},
statusText: {
fontSize: 12,
fontWeight: '600',
textTransform: 'uppercase',
},
infoRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: 16,
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: '#F3F4F6',
marginBottom: 16,
},
infoItem: {
alignItems: 'flex-start',
},
infoLabel: {
fontSize: 14,
color: '#6B7280',
marginBottom: 4,
},
infoValue: {
fontSize: 16,
fontWeight: '600',
color: '#111827',
textTransform: 'capitalize',
},
progressContainer: {
marginBottom: 16,
},
progressLabels: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
participantText: {
fontSize: 14,
color: '#6B7280',
},
participantCount: {
fontSize: 14,
fontWeight: '600',
color: '#111827',
},
progressBarBackground: {
height: 8,
backgroundColor: '#E5E7EB',
borderRadius: 4,
overflow: 'hidden',
},
progressBarFill: {
height: '100%',
backgroundColor: '#4F46E5',
borderRadius: 4,
},
footer: {
alignItems: 'center',
marginTop: 8,
},
dateText: {
fontSize: 14,
color: '#4B5563',
fontWeight: '500',
}, },
}); });
+1 -1
View File
@@ -64,7 +64,7 @@ export default function AppNavigator() {
<Stack.Screen name="GroupReview" component={GroupReview} options={{ headerShown: false }} /> <Stack.Screen name="GroupReview" component={GroupReview} options={{ headerShown: false }} />
<Stack.Screen name="BecomeOrg" component={BecomeOrg} options={{ headerShown: false }} /> <Stack.Screen name="BecomeOrg" component={BecomeOrg} options={{ headerShown: false }} />
<Stack.Screen name="YourContacts" component={YourContacts} options={{ headerShown: false }} /> <Stack.Screen name="YourContacts" component={YourContacts} options={{ headerShown: false }} />
{/* <Stack.Screen name="EditGroup" component={EditGroupScreen} options={{ headerShown: false }} /> */} <Stack.Screen name="EditGroup" component={EditGroupScreen} options={{ headerShown: false }} />
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
+261 -190
View File
@@ -13,6 +13,7 @@ import {
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
import AppView from '../../components/AppView'; import AppView from '../../components/AppView';
import { Picker } from '@react-native-picker/picker';
// --- Type Definitions --- // --- Type Definitions ---
interface Participant { interface Participant {
@@ -27,216 +28,284 @@ export interface GroupDetails {
id: string; id: string;
groupName: string; groupName: string;
description: string; description: string;
startDate: [number, number, number]; startDate: [number, number, number] | null;
endDate: [number, number, number] | null; endDate: [number, number, number] | null;
contributionAmount: number; contributionAmount: number;
minParticipants: number; minParticipants: number;
maxParticipants: number; maxParticipants: number;
portalCharge: number | null;
latePaymentCharge: number | null; latePaymentCharge: number | null;
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
status: 'PENDING' | 'ACTIVE' | 'COMPLETED'; customCycleDays: number;
withdrawalMethod: 'RANDOM' | 'SEQUENTIAL';
status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'APPROVED';
visibility: 'PUBLIC' | 'PRIVATE'; visibility: 'PUBLIC' | 'PRIVATE';
organizerId: string;
totalParticipants: number; totalParticipants: number;
participants: Participant[]; participants: Participant[];
} }
export interface EditGroupPayload { export interface EditGroupPayload {
groupName?: string; groupName: string;
description?: string; description: string;
maxParticipants?: number; maxParticipants: number;
contributionAmount: number;
customCycleDays: number | null;
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
latePaymentCharge: number;
visibility: 'PUBLIC' | 'PRIVATE';
} }
type EditGroupScreenProps = NativeStackScreenProps<RootStackParamList, 'EditGroup'>; type EditGroupScreenProps = NativeStackScreenProps<RootStackParamList, 'EditGroup'>;
type EditingSection = 'details' | 'financials' | null; type EditingField = keyof EditGroupPayload | null;
// --- Helper Components ---
const InfoRow: React.FC<{ label: string; value: string | number | null; isStatus?: boolean }> = ({ label, value, isStatus = false }) => (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}</Text>
<Text style={[styles.infoValue, isStatus && { color: value === 'ACTIVE' ? '#22c55e' : '#f97316' }]}>
{value ?? 'N/A'}
</Text>
</View>
);
const ParticipantRow: React.FC<{ participant: Participant }> = ({ participant }) => (
<View style={styles.participantRow}>
<View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={styles.participantName}>{`${participant.firstName} ${participant.lastName || ''}`.trim()}</Text>
{participant.role === 'ORGANIZER' && (
<View style={styles.organizerBadge}><Text style={styles.organizerBadgeText}>Organizer</Text></View>
)}
</View>
<Text style={styles.participantMobile}>{participant.mobile}</Text>
</View>
</View>
);
const FormInput: React.FC<{ label: string; value: string; onChangeText: (text: string) => void; multiline?: boolean; keyboardType?: 'default' | 'numeric' }> =
({ label, value, onChangeText, multiline = false, keyboardType = 'default' }) => (
<View style={styles.inputContainer}>
<Text style={styles.inputLabel}>{label}</Text>
<TextInput
style={[styles.input, multiline && styles.textArea]}
value={value}
onChangeText={onChangeText}
multiline={multiline}
keyboardType={keyboardType}
placeholderTextColor="#9CA3AF"
/>
</View>
);
// --- Main Screen Component --- // --- Main Screen Component ---
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => { const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const { groupId } = route.params; const { groupId } = route.params;
const [editingSection, setEditingSection] = useState<EditingSection>(null); const [editingField, setEditingField] = useState<EditingField>(null);
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null); const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
const [loading, setLoading] = useState(true); const [editableData, setEditableData] = useState<EditGroupPayload>({
const [saving, setSaving] = useState(false); groupName: '',
const [error, setError] = useState<string | null>(null); description: '',
maxParticipants: 0,
contributionAmount: 0,
customCycleDays: null,
cycleType: 'MONTHLY',
latePaymentCharge: 0,
visibility: 'PRIVATE',
});
const [groupName, setGroupName] = useState(''); const [loading, setLoading] = useState(true);
const [description, setDescription] = useState(''); const [saving, setSaving] = useState(false);
const [maxParticipants, setMaxParticipants] = useState(''); const [starting, setStarting] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchGroupDetails = useCallback(async () => { const fetchGroupDetails = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const response = await apiCall(`https://api.godemo.in/FundCollect/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string }; const response = await apiCall(`https://api.godemo.in/FundCollect/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
if (response.status === 'success' && response.data) { if (response.status === 'success' && response.data) {
setGroupDetails(response.data); setGroupDetails(response.data);
setGroupName(response.data.groupName); setEditableData({
setDescription(response.data.description); groupName: response.data.groupName,
setMaxParticipants(response.data.maxParticipants.toString()); description: response.data.description,
} else { maxParticipants: response.data.maxParticipants,
throw new Error(response.message || 'Failed to fetch group data.'); contributionAmount: response.data.contributionAmount,
customCycleDays: response.data.customCycleDays,
cycleType: response.data.cycleType,
latePaymentCharge: response.data.latePaymentCharge ?? 0,
visibility: response.data.visibility,
});
} else {
throw new Error(response.message || 'Failed to fetch group data.');
}
} catch (e: any) {
setError(e.message);
} finally {
setLoading(false);
}
}, [groupId]);
useEffect(() => {
fetchGroupDetails();
}, [fetchGroupDetails]);
const handleCancel = () => {
if (groupDetails) {
setEditableData({
groupName: groupDetails.groupName,
description: groupDetails.description,
maxParticipants: groupDetails.maxParticipants,
contributionAmount: groupDetails.contributionAmount,
customCycleDays: groupDetails.customCycleDays,
cycleType: groupDetails.cycleType,
latePaymentCharge: groupDetails.latePaymentCharge ?? 0,
visibility: groupDetails.visibility,
});
}
setEditingField(null);
};
const handleSave = async () => {
if (!groupDetails || !editingField) return;
if (editingField === 'maxParticipants') {
const newMaxParticipants = Number(editableData.maxParticipants);
if (newMaxParticipants < groupDetails.totalParticipants) {
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
return;
}
}
setSaving(true);
try {
await apiCall(`https://api.godemo.in/FundCollect/api/fund/groups/${groupDetails.id}/edit`, 'PUT', editableData);
Alert.alert('Success', 'Group has been updated.');
setEditingField(null);
await fetchGroupDetails();
} catch (e: any) {
Alert.alert('Save Error', e.message || 'An unexpected error occurred.');
} finally {
setSaving(false);
}
};
const handleStartGroup = async () => {
if (!groupDetails) return;
Alert.alert(
"Confirm Start",
"Are you sure you want to start this group? This action cannot be undone.",
[
{ text: "Cancel", style: "cancel" },
{
text: "Start",
onPress: async () => {
setStarting(true);
try {
await apiCall(`https://api.godemo.in/FundCollect/api/fund/group/start?groupId=${groupDetails.id}`, 'GET');
Alert.alert('Success', 'Group has been started successfully.');
await fetchGroupDetails();
} catch (e: any) {
Alert.alert('Start Error', e.message || 'An unexpected error occurred.');
} finally {
setStarting(false);
} }
} catch (e: any) { },
setError(e.message); style: 'destructive'
} finally {
setLoading(false);
} }
}, [groupId]); ]
);
};
useEffect(() => { const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
fetchGroupDetails(); setEditableData(prev => ({ ...prev, [field]: value }));
}, [fetchGroupDetails]); };
const handleCancel = () => { if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>;
if (groupDetails) { if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
setGroupName(groupDetails.groupName); if (!groupDetails) return null;
setDescription(groupDetails.description);
setMaxParticipants(groupDetails.maxParticipants.toString());
}
setEditingSection(null);
};
const handleSave = async (section: EditingSection) => { const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
if (!groupDetails || !section) return; const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
let payload: EditGroupPayload = {}; const renderInputField = (field: EditingField) => {
if (section === 'details') { if (!field || !editableData) return null;
payload = { groupName, description };
} else if (section === 'financials') {
const newMaxParticipants = parseInt(maxParticipants, 10);
if (newMaxParticipants < groupDetails.totalParticipants) {
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
return;
}
payload = { maxParticipants: newMaxParticipants };
}
setSaving(true); const commonProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
try {
await apiCall(`https://api.godemo.in/FundCollect/api/fund/groups/${groupDetails.id}/edit`, 'PUT', payload);
Alert.alert('Success', 'Group has been updated.');
setEditingSection(null);
await fetchGroupDetails();
} catch (e: any) {
Alert.alert('Save Error', e.message || 'An unexpected error occurred.');
} finally {
setSaving(false);
}
};
const CardActionButtons: React.FC<{ section: EditingSection }> = ({ section }) => ( switch(field) {
<View style={styles.cardActions}> case 'groupName':
<TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={handleCancel} disabled={saving}> return <TextInput {...commonProps} value={editableData.groupName} onChangeText={(text) => handleUpdateField('groupName', text)} />;
<Text style={[styles.buttonText, { color: '#374151' }]}>Cancel</Text> case 'description':
</TouchableOpacity> return <TextInput {...commonProps} style={[styles.input, styles.textArea]} value={editableData.description} onChangeText={(text) => handleUpdateField('description', text)} multiline />;
<TouchableOpacity style={[styles.button, styles.saveButton]} onPress={() => handleSave(section)} disabled={saving}> case 'contributionAmount':
{saving ? <ActivityIndicator color="#FFFFFF" size="small" /> : <Text style={styles.buttonText}>Save</Text>} case 'maxParticipants':
</TouchableOpacity> case 'latePaymentCharge':
case 'customCycleDays':
return <TextInput {...commonProps} value={String(editableData[field] ?? '')} onChangeText={(text) => handleUpdateField(field, text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
case 'cycleType':
return <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} />)}
</Picker>;
case 'visibility':
return <Picker selectedValue={editableData.visibility} onValueChange={(itemValue) => handleUpdateField('visibility', itemValue)} style={styles.picker}>
<Picker.Item label="PUBLIC" value="PUBLIC" />
<Picker.Item label="PRIVATE" value="PRIVATE" />
</Picker>;
default: return null;
}
}
const renderRow = (field: EditingField, label: string, displayValue: string | number) => {
return (
<View style={styles.fieldContainer}>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}</Text>
{editingField !== field && (
<View style={styles.valueContainer}>
<Text style={styles.infoValue}>{displayValue}</Text>
{isEditable && <TouchableOpacity onPress={() => setEditingField(field)} style={styles.editButton}>
<Text style={styles.editButtonText}>✎</Text>
</TouchableOpacity>}
</View>
)}
</View>
{editingField === field && isEditable && (
<View style={styles.editingContainer}>
{renderInputField(field)}
<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> </View>
); )
}
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>; return (
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>; <AppView paddingHorizontal={0}>
if (!groupDetails) return null; <ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}>
<Text style={styles.headerTitle}>Edit Group</Text>
const isEditable = groupDetails.status === 'PENDING'; {canStartGroup && (
<View style={styles.card}>
<Text style={styles.cardTitle}>Ready to Start?</Text>
<Text style={styles.startGroupText}>This group has met the minimum participant requirement.</Text>
<TouchableOpacity style={[styles.button, styles.startButton]} onPress={handleStartGroup} disabled={starting}>
{starting ? <ActivityIndicator color="#FFFFFF" /> : <Text style={styles.buttonText}>Start Group</Text>}
</TouchableOpacity>
</View>
)}
return ( <View style={styles.card}>
<AppView paddingHorizontal={0}> <Text style={styles.cardTitle}>Group Info</Text>
<ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}> {renderRow('groupName', 'Group Name', groupDetails.groupName)}
<Text style={styles.headerTitle}>Group Overview</Text> {renderRow('description', 'Description', groupDetails.description)}
</View>
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <Text style={styles.cardTitle}>Financial Rules</Text>
<Text style={styles.cardTitle}>Group Details</Text> {renderRow('contributionAmount', 'Contribution', `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`)}
{isEditable && editingSection !== 'details' && ( {renderRow('maxParticipants', 'Max Participants', `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}`)}
<TouchableOpacity onPress={() => setEditingSection('details')}><Text style={styles.editButtonText}>Edit</Text></TouchableOpacity> {renderRow('latePaymentCharge', 'Late Fee', `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}`)}
)} </View>
<View style={styles.card}>
<Text style={styles.cardTitle}>Configuration</Text>
{renderRow('cycleType', 'Payment Cycle', groupDetails.cycleType)}
{(editableData.cycleType === 'CUSTOM' || groupDetails.cycleType === 'CUSTOM') && renderRow('customCycleDays', 'Custom Days', `${editableData.customCycleDays ?? groupDetails.customCycleDays} days`)}
{renderRow('visibility', 'Visibility', groupDetails.visibility)}
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
{groupDetails.participants.length > 0 ? (
groupDetails.participants.map(p => (
<View key={p.id} style={styles.participantRow}>
<View>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text style={styles.participantName}>{`${p.firstName} ${p.lastName || ''}`.trim()}</Text>
{p.role === 'ORGANIZER' && <View style={styles.organizerBadge}><Text style={styles.organizerBadgeText}>Organizer</Text></View>}
</View>
<Text style={styles.participantMobile}>{p.mobile}</Text>
</View>
</View> </View>
{editingSection === 'details' ? ( ))
<> ) : (
<FormInput label="Group Name" value={groupName} onChangeText={setGroupName} /> <Text style={styles.noParticipantsText}>No participants yet.</Text>
<FormInput label="Description" value={description} onChangeText={setDescription} multiline /> )}
<CardActionButtons section="details" /> </View>
</> </ScrollView>
) : ( </AppView>
<> );
<Text style={styles.groupNameView}>{groupDetails.groupName}</Text>
<Text style={styles.descriptionView}>{groupDetails.description}</Text>
</>
)}
</View>
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}>Financial Rules</Text>
{isEditable && editingSection !== 'financials' && (
<TouchableOpacity onPress={() => setEditingSection('financials')}><Text style={styles.editButtonText}>Edit</Text></TouchableOpacity>
)}
</View>
<InfoRow label="Contribution" value={`₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`} />
{editingSection === 'financials' ? (
<>
<FormInput label="Max Participants" value={maxParticipants} onChangeText={setMaxParticipants} keyboardType="numeric" />
<CardActionButtons section="financials" />
</>
) : (
<InfoRow label="Participants" value={`${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}`} />
)}
<InfoRow label="Payment Cycle" value={groupDetails.cycleType} />
<InfoRow label="Group Status" value={groupDetails.status} isStatus />
</View>
<View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
{groupDetails.participants.length > 0 ? (
groupDetails.participants.map(p => <ParticipantRow key={p.id} participant={p} />)
) : (
<Text style={styles.noParticipantsText}>No participants have joined yet.</Text>
)}
</View>
</ScrollView>
</AppView>
);
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@@ -247,33 +316,35 @@ const styles = StyleSheet.create({
headerTitle: { fontSize: 28, fontWeight: 'bold', color: '#111827', marginBottom: 20 }, headerTitle: { fontSize: 28, fontWeight: 'bold', color: '#111827', marginBottom: 20 },
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: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10, marginBottom: 16 }, cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937', marginBottom: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10 },
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' },
editButtonText: { color: '#4F46E5', fontSize: 15, fontWeight: '600' },
groupNameView: { fontSize: 22, fontWeight: '700', color: '#1F2937', marginBottom: 8 }, editButton: { padding: 8, marginLeft: 8 },
descriptionView: { fontSize: 16, color: '#6B7280', lineHeight: 24 }, editButtonText: { color: '#4F46E5', fontSize: 20, fontWeight: 'bold' },
inputContainer: { marginBottom: 16 }, fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingVertical: 12 },
inputLabel: { fontSize: 14, color: '#6B7280', marginBottom: 8, fontWeight: '500' }, infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 14, fontSize: 16 },
textArea: { height: 100, textAlignVertical: 'top' },
infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' },
infoLabel: { fontSize: 16, color: '#6B7280' }, infoLabel: { fontSize: 16, color: '#6B7280' },
infoValue: { fontSize: 16, fontWeight: '600', textTransform: 'capitalize' }, infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
valueContainer: { flexDirection: 'row', alignItems: 'center' },
participantRow: { paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' }, editingContainer: { marginTop: 12 },
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, padding: 0, margin:0 },
participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' },
participantName: { fontSize: 16, fontWeight: '600', color: '#111827' }, participantName: { fontSize: 16, fontWeight: '600', color: '#111827' },
participantMobile: { fontSize: 14, color: '#6B7280', marginTop: 4 }, participantMobile: { fontSize: 14, color: '#6B7280', marginTop: 4 },
organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 }, organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 },
organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' }, organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' },
noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 20 }, noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 20 },
cardActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 20, borderTopWidth: 1, borderTopColor: '#E5E7EB', paddingTop: 16 }, cardActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#E5E7EB' },
button: { borderRadius: 10, paddingVertical: 10, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center' }, button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center', justifyContent: 'center' },
saveButton: { backgroundColor: '#4F46E5' }, saveButton: { backgroundColor: '#4F46E5' },
cancelButton: { backgroundColor: '#E5E7EB' }, cancelButton: { backgroundColor: '#E5E7EB' },
startButton: { backgroundColor: '#22c55e', marginTop: 10 },
startGroupText: { fontSize: 15, color: '#6B7280', lineHeight: 22, marginBottom: 10},
buttonText: { color: '#FFFFFF', fontSize: 15, fontWeight: '600' }, buttonText: { color: '#FFFFFF', fontSize: 15, fontWeight: '600' },
}); });
+46 -3
View File
@@ -18,7 +18,7 @@ import { APPROVE_REQUEST_URL, BASE_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_UR
import colors from "../../components/colors"; import colors from "../../components/colors";
import AppView from "../../components/AppView"; import AppView from "../../components/AppView";
import { apiCall } from "../../utils/ApiHelper"; import { apiCall } from "../../utils/ApiHelper";
import * as Keychain from "react-native-keychain";
import { MyGroup, JoinedGroup, JoinRequest, ApiResponse } from '../../types'; import { MyGroup, JoinedGroup, JoinRequest, ApiResponse } from '../../types';
import { MyGroupCard } from "../../components/MyGroupCard"; import { MyGroupCard } from "../../components/MyGroupCard";
import { JoinedGroupCard } from "../../components/JoinedGroupCard"; import { JoinedGroupCard } from "../../components/JoinedGroupCard";
@@ -38,14 +38,57 @@ const ManagementScreen: FC = () => {
const [requests, setRequests] = useState<JoinRequest[]>([]); const [requests, setRequests] = useState<JoinRequest[]>([]);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const userRole = user.role; const [userRole,setUserRole] = useState<string | null>(null)
const userStatus = async () => {
try {
const credentials = await Keychain.getGenericPassword();
if (!credentials) {
Alert.alert("Error", "You are not logged in");
return;
}
const token = credentials.password;
setLoading(true);
setError(null);
const response = await fetch(`${BASE_URL}user/profile/status`, {
method: "GET",
headers: {
Accept: "*/*",
Authorization: `Bearer ${token}`,
},
});
const json = await response.json();
console.log("👤 Profile Status Response:", json.data.role);
if (json.status === "success") {
setUserRole(json.data.role);
if (json.data.profileStatus === false) {
}
} else {
setError(json.message || "Failed to fetch status");
}
} catch (err) {
setError("Something went wrong. Please try again.");
} finally {
setLoading(false);
}
};
useFocusEffect(
useCallback(() => {
userStatus();
}, [])
);
console.log(" abin user : ", user)
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
if (loading) return; if (loading) return;
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
console.log(" abin user : ", userRole)
if (userRole === "ORGANIZER") { if (userRole === "ORGANIZER") {
// Fetch all data concurrently // Fetch all data concurrently
const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([ const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([