Files
circle-collect/src/screens/InviteScreen.tsx
T
2025-10-01 15:43:42 +05:30

207 lines
7.9 KiB
TypeScript

import { NativeStackScreenProps } from '@react-navigation/native-stack';
import React, { useState, useEffect, useCallback, FC } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
} from 'react-native';
import { RootStackParamList } from '../navigation/AppNavigator'; // Adjust path
import { apiCall } from '../utils/ApiHelper'; // Adjust path
import AppView from '../components/AppView'; // Adjust path
import colors from '../components/colors'; // Adjust path
// --- Type Definitions ---
interface Participant {
id: string;
firstName: string;
lastName: string | null;
mobile: string;
role: 'ORGANIZER' | 'USER';
}
interface GroupDetails {
id: string;
groupName: string;
description: string;
contributionAmount: number;
maxParticipants: number;
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'APPROVED';
organizerFirstName: string;
organizerLastName: string | null;
totalParticipants: number;
participants: Participant[];
}
type InviteScreenProps = NativeStackScreenProps<RootStackParamList, 'Invite'>; // Assuming 'Invite' is in your stack
// --- Main Screen Component ---
const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
const { groupId,code} = route.params;
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
const [loading, setLoading] = useState(true);
const [joining, setJoining] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchGroupDetails = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await apiCall(`http://192.168.1.9:9060/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
if (response.status === 'success' && response.data) {
setGroupDetails(response.data);
} else {
throw new Error(response.message || 'Failed to fetch group invitation details.');
}
} catch (e: any) {
setError(e.message);
} finally {
setLoading(false);
}
}, [groupId]);
useEffect(() => {
fetchGroupDetails();
}, [fetchGroupDetails]);
const handleJoinGroup = async () => {
if (!groupDetails) return;
setJoining(true);
try {
// Replace with your actual API call to join the group
// For now, it just shows an alert.
await apiCall(`http://192.168.1.9:9060/api/fund/groups/join/invite/${code}`, 'POST');
Alert.alert('Success!', `You have requested to join "${groupDetails.groupName}".`);
} catch (e: any) {
Alert.alert('Join Error', e.message || 'An unexpected error occurred.');
} finally {
setJoining(false);
}
};
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color={colors.primary} /></View>;
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
if (!groupDetails) return null;
const organizerName = `${groupDetails.organizerFirstName} ${groupDetails.organizerLastName || ''}`.trim();
return (
<AppView>
<ScrollView contentContainerStyle={styles.contentContainer}>
<Text style={styles.headerTitle}>You're Invited!</Text>
<Text style={styles.headerSubtitle}>Join the group <Text style={{ fontWeight: 'bold' }}>{groupDetails.groupName}</Text></Text>
{/* Group Info Card */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Group Details </Text>
<InfoRow label="Organizer" value={organizerName} />
<InfoRow label="Contribution" value={`₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`} />
<InfoRow label="Payment Cycle" value={groupDetails.cycleType} />
<InfoRow label="Status" value={groupDetails.status} />
<InfoRow label="Code" value={code} />
</View>
{/* Participants Card */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants} / {groupDetails.maxParticipants})</Text>
{groupDetails.participants.length > 0 ? (
groupDetails.participants.map(p => (
<View key={p.id} style={styles.participantRow}>
<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.noParticipantsText}>Be the first to join!</Text>
)}
</View>
</ScrollView>
{/* Floating Join Button */}
<View style={styles.floatingButtonContainer}>
<TouchableOpacity
style={[styles.joinButton, groupDetails.status !== 'PENDING' && groupDetails.status !== 'APPROVED' ? styles.disabledButton : {}]}
onPress={handleJoinGroup}
disabled={joining || (groupDetails.status !== 'PENDING' && groupDetails.status !== 'APPROVED')}
>
{joining ? (
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.joinButtonText}>
{groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED' ? 'Join Group' : `Group is ${groupDetails.status}`}
</Text>
)}
</TouchableOpacity>
</View>
</AppView>
);
};
// Helper component for displaying rows
const InfoRow = ({ label, value }: { label: string; value: string | number }) => (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}</Text>
<Text style={styles.infoValue}>{value}</Text>
</View>
);
const styles = StyleSheet.create({
centered: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F3F4F6' },
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center', padding: 20 },
contentContainer: { padding: 16, paddingBottom: 100 }, // Add padding to avoid overlap with button
headerTitle: { fontSize: 32, fontWeight: 'bold', color: '#111827', textAlign: 'center' },
headerSubtitle: { fontSize: 18, color: '#4B5563', textAlign: 'center', marginBottom: 24, marginTop: 4 },
card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 },
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937', marginBottom: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10 },
infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 },
infoLabel: { fontSize: 16, color: '#6B7280' },
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10 },
participantName: { fontSize: 16, color: '#1F2937' },
organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12 },
organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' },
noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 10 },
floatingButtonContainer: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
alignItems: 'center',
},
joinButton: {
backgroundColor: colors.primary,
borderRadius: 14,
paddingVertical: 16,
paddingHorizontal: 32,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 4,
width: '80%',
alignItems: 'center',
},
joinButtonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: 'bold',
},
disabledButton: {
backgroundColor: '#9CA3AF', // Gray color when disabled
},
});
export default InviteScreen;