207 lines
7.9 KiB
TypeScript
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(`https://exotic-humpback-mistakenly.ngrok-free.app/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(`https://exotic-humpback-mistakenly.ngrok-free.app/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;
|
|
|