work done
This commit is contained in:
@@ -46,6 +46,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal:12
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 18,
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
LayoutAnimation,
|
||||
UIManager,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from "@react-native-vector-icons/ionicons";import colors from "../../../components/colors"; // Assuming a colors file
|
||||
|
||||
// Enable LayoutAnimation for Android
|
||||
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
||||
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||||
}
|
||||
|
||||
// --- TypeScript Interfaces for Type Safety ---
|
||||
|
||||
interface Contribution {
|
||||
transactionId: string;
|
||||
userName: string;
|
||||
baseAmount: number;
|
||||
lateFee: number;
|
||||
waivedAmount: number;
|
||||
totalAmount: number;
|
||||
status: 'SUCCESS' | 'PENDING' | 'FAILED';
|
||||
paidDate: [number, number, number];
|
||||
}
|
||||
|
||||
interface Withdrawal {
|
||||
transactionId: string;
|
||||
userName: string;
|
||||
baseAmount: number;
|
||||
totalAmount: number;
|
||||
status: 'SUCCESS' | 'PENDING' | 'FAILED';
|
||||
paidDate: [number, number, number];
|
||||
}
|
||||
|
||||
interface Cycle {
|
||||
cycleNumber: number;
|
||||
cycleStart: [number, number, number];
|
||||
cycleEnd: [number, number, number];
|
||||
expectedContribution: number;
|
||||
contributions: Contribution[];
|
||||
withdrawal: Withdrawal | null;
|
||||
totalParticipants: number;
|
||||
totalCollected: number;
|
||||
collectionPercentage: number;
|
||||
paidParticipants: number;
|
||||
pendingParticipants: number;
|
||||
}
|
||||
|
||||
interface CycleDashboardData {
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
totalCycles: number;
|
||||
currentCycle: number;
|
||||
contributionAmount: number;
|
||||
cycleType: 'MONTHLY' | 'WEEKLY' | 'DAILY';
|
||||
status: 'COMPLETED' | 'ACTIVE' | 'PENDING';
|
||||
cycles: Cycle[];
|
||||
totalParticipants: number;
|
||||
totalCollected: number;
|
||||
}
|
||||
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
||||
const formatDate = (dateArray: [number, number, number]): string => {
|
||||
if (!dateArray || dateArray.length < 3) return 'N/A';
|
||||
const [year, month, day] = dateArray;
|
||||
return new Date(year, month - 1, day).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number): string => {
|
||||
return new Intl.NumberFormat('en-IN', {
|
||||
style: 'currency',
|
||||
currency: 'INR',
|
||||
minimumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
|
||||
// --- Child Components ---
|
||||
|
||||
const CycleItem = ({ item, isExpanded, onToggle }: { item: Cycle, isExpanded: boolean, onToggle: () => void }) => {
|
||||
return (
|
||||
<View style={styles.cycleCard}>
|
||||
<TouchableOpacity onPress={onToggle} style={styles.cycleHeader}>
|
||||
<View>
|
||||
<Text style={styles.cycleTitle}>Cycle {item.cycleNumber}</Text>
|
||||
<Text style={styles.cycleDateRange}>
|
||||
{formatDate(item.cycleStart)} - {formatDate(item.cycleEnd)}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name={isExpanded ? 'chevron-up' : 'chevron-down'} size={24} color={colors.black} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{isExpanded && (
|
||||
<View style={styles.cycleDetails}>
|
||||
{/* Withdrawal Info */}
|
||||
{item.withdrawal && (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Withdrawal</Text>
|
||||
<View style={styles.withdrawalCard}>
|
||||
<Ionicons name="arrow-down-circle-outline" size={32} color={colors.Home} />
|
||||
<View style={styles.withdrawalInfo}>
|
||||
<Text style={styles.withdrawalUser}>{item.withdrawal.userName}</Text>
|
||||
<Text style={styles.withdrawalAmount}>{formatCurrency(item.withdrawal.totalAmount)}</Text>
|
||||
</View>
|
||||
<Text style={styles.withdrawalDate}>{formatDate(item.withdrawal.paidDate)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Contributions List */}
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>Contributions ({item.paidParticipants}/{item.totalParticipants})</Text>
|
||||
{item.contributions.map(contribution => (
|
||||
<View key={contribution.transactionId} style={styles.contributionItem}>
|
||||
<Ionicons name="person-circle-outline" size={28} color="#475569" />
|
||||
<View style={{ flex: 1, marginLeft: 10 }}>
|
||||
<Text style={styles.contributorName}>{contribution.userName}</Text>
|
||||
<Text style={styles.contributionDate}>{formatDate(contribution.paidDate)}</Text>
|
||||
</View>
|
||||
<Text style={styles.contributionAmount}>{formatCurrency(contribution.totalAmount)}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// --- Main Dashboard Component ---
|
||||
|
||||
const CycleDashboard = ({ data }: { data: CycleDashboardData }) => {
|
||||
const [expandedCycle, setExpandedCycle] = useState<number | null>(data.currentCycle);
|
||||
|
||||
const handleToggle = (cycleNumber: number) => {
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
setExpandedCycle(expandedCycle === cycleNumber ? null : cycleNumber);
|
||||
};
|
||||
|
||||
const renderHeader = () => (
|
||||
<View style={styles.headerContainer}>
|
||||
<Text style={styles.groupName}>{data.groupName}</Text>
|
||||
<View style={styles.summaryRow}>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Total Collected</Text>
|
||||
<Text style={styles.summaryValue}>{formatCurrency(data.totalCollected)}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Status</Text>
|
||||
<Text style={[styles.summaryValue, { color: data.status === 'COMPLETED' ? colors.Settings : colors.Home }]}>{data.status}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Cycles</Text>
|
||||
<Text style={styles.summaryValue}>{data.currentCycle}/{data.totalCycles}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={data.cycles}
|
||||
keyExtractor={(item) => item.cycleNumber.toString()}
|
||||
renderItem={({ item }) => (
|
||||
<CycleItem
|
||||
item={item}
|
||||
isExpanded={expandedCycle === item.cycleNumber}
|
||||
onToggle={() => handleToggle(item.cycleNumber)}
|
||||
/>
|
||||
)}
|
||||
ListHeaderComponent={renderHeader}
|
||||
contentContainerStyle={styles.container}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CycleDashboard;
|
||||
|
||||
|
||||
// --- Styles ---
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
paddingHorizontal: 15,
|
||||
paddingBottom: 20,
|
||||
},
|
||||
// Header
|
||||
headerContainer: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
groupName: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#1e293b',
|
||||
marginBottom: 15,
|
||||
},
|
||||
summaryRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
summaryBox: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
summaryLabel: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 4,
|
||||
},
|
||||
summaryValue: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#0f172a',
|
||||
},
|
||||
// Cycle Item Card
|
||||
cycleCard: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
marginBottom: 15,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.08,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowRadius: 8,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
cycleHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
},
|
||||
cycleTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#1e293b',
|
||||
},
|
||||
cycleDateRange: {
|
||||
fontSize: 13,
|
||||
color: '#64748b',
|
||||
marginTop: 2,
|
||||
},
|
||||
cycleDetails: {
|
||||
paddingHorizontal: 16,
|
||||
paddingBottom: 16,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#f1f5f9',
|
||||
},
|
||||
section: {
|
||||
marginTop: 16,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#334155',
|
||||
marginBottom: 12,
|
||||
},
|
||||
// Withdrawal Card
|
||||
withdrawalCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.pastelPink, // Example color
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
withdrawalInfo: {
|
||||
flex: 1,
|
||||
marginLeft: 12,
|
||||
},
|
||||
withdrawalUser: {
|
||||
fontSize: 15,
|
||||
fontWeight: 'bold',
|
||||
color: colors.Home
|
||||
},
|
||||
withdrawalAmount: {
|
||||
fontSize: 14,
|
||||
color: colors.Home
|
||||
},
|
||||
withdrawalDate: {
|
||||
fontSize: 12,
|
||||
color: '#475569'
|
||||
},
|
||||
// Contribution Item
|
||||
contributionItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#f1f5f9',
|
||||
},
|
||||
contributorName: {
|
||||
fontSize: 15,
|
||||
color: '#1e293b',
|
||||
},
|
||||
contributionDate: {
|
||||
fontSize: 12,
|
||||
color: '#64748b',
|
||||
},
|
||||
contributionAmount: {
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
color: '#1e293b',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
TouchableOpacity,
|
||||
FlatList,
|
||||
Animated,
|
||||
ToastAndroid,
|
||||
Alert,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Modal,
|
||||
} from "react-native";
|
||||
import { useRoute, RouteProp } from "@react-navigation/native";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import AppView from "../../../components/AppView";
|
||||
import colors from "../../../components/colors";
|
||||
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
||||
@@ -17,30 +17,62 @@ import Header from "../../../components/common/Header";
|
||||
import { apiCall } from "../../../utils/ApiHelper";
|
||||
import { BASE_URL, CONTRIBUTE, GROUP_ALL_MEMBER } from "../../../utils/ApiUrls";
|
||||
import ContributionScreen from "../../ContributionScreen";
|
||||
import CycleDashboard from "./CycleDashboard";
|
||||
|
||||
// Define type for a member
|
||||
interface Member {
|
||||
id: string;
|
||||
name: string;
|
||||
role: "Organizer" | "Member";
|
||||
contribution: number;
|
||||
data: any
|
||||
}
|
||||
const CustomDropdown = ({ items, selectedValue, onValueChange, placeholder = "Select an item" }:any) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const selectedLabel = items.find((item:any) => item.value === selectedValue)?.label || placeholder;
|
||||
|
||||
// Define route params type
|
||||
return (
|
||||
<View style={styles.dropdownWrapper}>
|
||||
<TouchableOpacity onPress={() => setOpen(true)} style={styles.dropdownHeader}>
|
||||
<Text style={styles.headerText}>{selectedLabel}</Text>
|
||||
<Ionicons name='chevron-down' size={22} color={colors.black} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<Modal
|
||||
transparent
|
||||
animationType="fade"
|
||||
visible={open}
|
||||
onRequestClose={() => setOpen(false)}
|
||||
>
|
||||
<TouchableOpacity style={styles.modalOverlay} onPress={() => setOpen(false)}>
|
||||
<View style={styles.dropdownListContainer}>
|
||||
<ScrollView>
|
||||
{items.map(item => (
|
||||
<TouchableOpacity
|
||||
key={item.value}
|
||||
onPress={() => {
|
||||
onValueChange(item.value);
|
||||
setOpen(false);
|
||||
}}
|
||||
style={[
|
||||
styles.dropdownItem,
|
||||
item.value === selectedValue && styles.selectedItem
|
||||
]}
|
||||
>
|
||||
<Text style={styles.itemText}>{item.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const GroupDetails = () => {
|
||||
const route = useRoute<any>();
|
||||
const { id, groupName, description, contributionAmount, currentParticipants } = route.params;
|
||||
|
||||
const [memberData, setMemberData] = useState<Member[]>([]);
|
||||
const [memberData, setMemberData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [openModal, setOpenModal] = useState(false)
|
||||
const [payable, setPayable] = useState() as any
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [payable, setPayable] = useState<any>();
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [cycleDashboardData, setCycleDashboardData] = useState<any>();
|
||||
const [selectedCycleNumber, setSelectedCycleNumber] = useState(null);
|
||||
const [cycleItems, setCycleItems] = useState([]);
|
||||
|
||||
console.log("route param ggggggggggg : ", route?.params)
|
||||
// Group object
|
||||
const group = {
|
||||
id: route.params.item?.id,
|
||||
name: route.params.item?.groupName,
|
||||
@@ -51,224 +83,291 @@ const GroupDetails = () => {
|
||||
members: memberData,
|
||||
};
|
||||
|
||||
const progress = Math.min(group.collected / group.goal, 1);
|
||||
const getCycleDashboard = async () => {
|
||||
try {
|
||||
const response = await apiCall(`${BASE_URL}payments/groups/${group.id}/cycle-dashboard`, "GET") as any;
|
||||
const responseData = response.data;
|
||||
setCycleDashboardData(responseData);
|
||||
|
||||
// Animation refs
|
||||
const animatedValues = useRef<Animated.Value[]>([]);
|
||||
|
||||
// useEffect(() => {
|
||||
// animatedValues.current = group.members.map(() => new Animated.Value(0));
|
||||
// }, [group.members]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const animations = group.members.map((_, i) =>
|
||||
// Animated.timing(animatedValues.current[i], {
|
||||
// toValue: 1,
|
||||
// duration: 400,
|
||||
// delay: i * 100,
|
||||
// useNativeDriver: true,
|
||||
// })
|
||||
// );
|
||||
// Animated.stagger(100, animations).start();
|
||||
// }, [group.members]);
|
||||
|
||||
// Fetch group members
|
||||
useEffect(() => {
|
||||
const handleGroupDetails = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${route?.params?.item?.id}`, "GET") as any;
|
||||
console.log("Group details response:", res);
|
||||
setMemberData(res.data);
|
||||
ToastAndroid.show("Group details fetched successfully!", ToastAndroid.SHORT);
|
||||
} catch (error: any) {
|
||||
console.error("Error fetching group details:", error);
|
||||
ToastAndroid.show(error?.message || "Failed to fetch group details", ToastAndroid.SHORT);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (responseData?.cycles) {
|
||||
const items = responseData.cycles.map(cycle => ({
|
||||
label: `Cycle ${cycle.cycleNumber}`,
|
||||
value: cycle.cycleNumber
|
||||
}));
|
||||
setCycleItems(items);
|
||||
if (responseData.currentCycle) {
|
||||
setSelectedCycleNumber(responseData.currentCycle);
|
||||
}
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
ToastAndroid.show("Failed to fetch cycle data", ToastAndroid.SHORT);
|
||||
}
|
||||
};
|
||||
|
||||
handleGroupDetails();
|
||||
}, [id]);
|
||||
const fetchGroupDetails = async () => {
|
||||
try {
|
||||
const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${group.id}`, "GET") as any;
|
||||
console.log("Group Members Response: ", res);
|
||||
setMemberData(res.data);
|
||||
} catch (error: any) {
|
||||
ToastAndroid.show(error?.message || "Failed to fetch group details", ToastAndroid.SHORT);
|
||||
}
|
||||
};
|
||||
|
||||
const handleContribution = async () => {
|
||||
try {
|
||||
const res = await apiCall(`${CONTRIBUTE}${group.id}/calculate-contribution`, 'GET') as any
|
||||
setPayable(res.data)
|
||||
console.log("contribution res : ", res)
|
||||
const res = await apiCall(`${CONTRIBUTE}${group.id}/calculate-contribution`, 'GET') as any;
|
||||
setPayable(res.data);
|
||||
} catch (error) {
|
||||
console.log("contribution error : ", error)
|
||||
console.log("Error in handleContribution: ", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// useEffect(()=>{
|
||||
// if(group?.id)
|
||||
// handleContribution()
|
||||
// },[])
|
||||
const fetchAllData = useCallback(async () => {
|
||||
setLoading(true);
|
||||
await Promise.all([
|
||||
fetchGroupDetails(),
|
||||
handleContribution(),
|
||||
getCycleDashboard()
|
||||
]);
|
||||
setLoading(false);
|
||||
}, [group.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (group.id) {
|
||||
fetchAllData();
|
||||
}
|
||||
}, [group.id, fetchAllData]);
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await fetchAllData();
|
||||
setRefreshing(false);
|
||||
}, [fetchAllData]);
|
||||
|
||||
const confirmPayment = async () => {
|
||||
try {
|
||||
const res = await apiCall(`${BASE_URL}payments/contribute/${group?.id}?amount=${payable?.totalPayable}`, 'POST') as any
|
||||
console.log("payment response : ", res)
|
||||
if (res.status) {
|
||||
ToastAndroid.show('Payment done!', ToastAndroid.SHORT)
|
||||
}
|
||||
if (res.status === 400) {
|
||||
ToastAndroid.show(res?.data?.message || 'Payment done!', ToastAndroid.SHORT)
|
||||
|
||||
const res = await apiCall(`${BASE_URL}payments/contribute/${group?.id}?amount=${payable?.totalPayable}`, 'POST') as any;
|
||||
if (res.status === 200 || res.status === 201) {
|
||||
ToastAndroid.show('Payment done!', ToastAndroid.SHORT);
|
||||
onRefresh();
|
||||
} else {
|
||||
ToastAndroid.show(res?.data?.message || 'Payment failed!', ToastAndroid.SHORT);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("payment error : ", error)
|
||||
ToastAndroid.show("An error occurred during payment.", ToastAndroid.SHORT);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
const renderMember = ({ item, index }: { item: any; index: number }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.memberCard,
|
||||
{
|
||||
// opacity: animatedValues.current[index],
|
||||
// transform: [
|
||||
// {
|
||||
// translateY: animatedValues.current[index].interpolate({
|
||||
// inputRange: [0, 1],
|
||||
// outputRange: [20, 0],
|
||||
// }),
|
||||
// },
|
||||
// ],
|
||||
borderColor: route.params?.color ? route.params?.color : colors.Home
|
||||
},
|
||||
]}
|
||||
>
|
||||
<View style={{ flexDirection: "row", gap: 10 }}>
|
||||
<Ionicons name="person-circle-outline" size={40} color={route.params?.color ? route.params?.color : colors.primary} />
|
||||
<View>
|
||||
<Text style={styles.memberName}>
|
||||
{item.firstName} {item.lastName} {item.role === "Organizer" ? "(Organizer)" : "(Member)"}
|
||||
</Text>
|
||||
<Text style={styles.memberDetail}>Mobile: {item.mobile}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
const selectedCycleData = cycleDashboardData?.cycles.find(c => c.cycleNumber === selectedCycleNumber);
|
||||
|
||||
return (
|
||||
<AppView>
|
||||
<Header title={group.name} arrowColor={route.params?.color ? route.params?.color : colors.Home} titleColor={route.params?.color ? route.params?.color : colors.Home} />
|
||||
<Text style={styles.description}>{group.description}</Text>
|
||||
<AppView paddingHorizontal={0} style={{ flex: 1 }}>
|
||||
<ScrollView
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing || loading} onRefresh={onRefresh} colors={[colors.black]} />
|
||||
}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
<Header title={group.name} />
|
||||
<Text style={styles.description}>{group.description}</Text>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<View style={styles.progressContainer}>
|
||||
<View style={styles.progressBar}>
|
||||
<View style={{ flex: progress, backgroundColor: route.params?.color ? route.params?.color : colors.Home }} />
|
||||
<View style={{ flex: 1 - progress }} />
|
||||
<View style={styles.statsRow}>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statValue}>{group.members.length ?? 0}</Text>
|
||||
<Text style={styles.statLabel}>Members</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statValue}>₹{group.collected}</Text>
|
||||
<Text style={styles.statLabel}>Contribution</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statValue}>₹{group.goal}</Text>
|
||||
<Text style={styles.statLabel}>Total Amount</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.amount}>₹{group.collected} / ₹{group.goal}</Text>
|
||||
</View>
|
||||
|
||||
{/* Stats */}
|
||||
<View style={styles.statsRow}>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statValue}>{group.members.length ?? []}</Text>
|
||||
<Text style={styles.statLabel}>Members</Text>
|
||||
{cycleItems.length > 0 && (
|
||||
<CustomDropdown
|
||||
items={cycleItems}
|
||||
selectedValue={selectedCycleNumber}
|
||||
onValueChange={setSelectedCycleNumber}
|
||||
placeholder="Select a Cycle"
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedCycleData && cycleDashboardData && (
|
||||
<CycleDashboard data={{ ...cycleDashboardData, cycles: [selectedCycleData] }} />
|
||||
)}
|
||||
|
||||
<View style={styles.membersHeader}>
|
||||
<Text style={styles.sectionTitle}>Members</Text>
|
||||
<View style={styles.statusBadge}>
|
||||
<Text style={styles.statusText}>{group.status}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statValue}>₹{group.collected}</Text>
|
||||
<Text style={styles.statLabel}>Collected</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={styles.statValue}>₹{group.goal}</Text>
|
||||
<Text style={styles.statLabel}>Goal</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Members List */}
|
||||
<View style={styles.statsRow}>
|
||||
<Text style={styles.sectionTitle}>Members</Text>
|
||||
<Text style={[styles.sectionTitle, { backgroundColor: route.params?.color ? colors.pastelOrange : colors.pastelPink, color: route.params?.color ? colors.Settings : colors.Home, paddingHorizontal: 5 }]}>{group.status}</Text>
|
||||
</View>
|
||||
{memberData.map((member:any) => (
|
||||
<View key={member.userId} style={styles.memberCard}>
|
||||
<Ionicons name="person" size={40} color={colors.black} />
|
||||
<View style={styles.memberInfo}>
|
||||
<Text style={styles.memberName}>
|
||||
{member.firstName} {member.role === "Organizer" ? "(Organizer)" : ""}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
|
||||
<FlatList
|
||||
data={group.members}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderMember}
|
||||
contentContainerStyle={styles.membersListContainer}
|
||||
/>
|
||||
|
||||
{/* Actions */}
|
||||
{
|
||||
group.status === 'ACTIVE' &&
|
||||
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity onPress={() => {
|
||||
setOpenModal(true)
|
||||
handleContribution()
|
||||
}}
|
||||
style={[styles.button, { backgroundColor: route.params?.color ? route.params?.color : colors.Home }]}>
|
||||
{/* Button at the bottom inside scroll (no absolute positioning) */}
|
||||
{group.status === 'ACTIVE' && !payable?.hasPaid && (
|
||||
<TouchableOpacity onPress={() => setOpenModal(true)} style={styles.button}>
|
||||
<Text style={styles.buttonText}>Contribute</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
{
|
||||
openModal && <ContributionScreen data={payable} visible={openModal} onClose={() => setOpenModal(!openModal)} onConfirm={() => {
|
||||
confirmPayment()
|
||||
setOpenModal(false)
|
||||
}} />
|
||||
}
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{openModal && (
|
||||
<ContributionScreen
|
||||
data={payable}
|
||||
visible={openModal}
|
||||
onClose={() => setOpenModal(false)}
|
||||
onConfirm={() => {
|
||||
confirmPayment();
|
||||
setOpenModal(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</AppView>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupDetails;
|
||||
|
||||
// Styles remain mostly unchanged
|
||||
const styles = StyleSheet.create({
|
||||
dropdownWrapper: {
|
||||
marginHorizontal: 15,
|
||||
marginBottom: 20,
|
||||
},
|
||||
dropdownHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: '#ddd',
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
padding: 20,
|
||||
},
|
||||
dropdownListContainer: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
maxHeight: 300,
|
||||
},
|
||||
dropdownItem: {
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
selectedItem: {
|
||||
backgroundColor: colors.pastelOrange,
|
||||
borderRadius: 6,
|
||||
},
|
||||
itemText: {
|
||||
fontSize: 15,
|
||||
color: colors.black,
|
||||
},
|
||||
description: {
|
||||
fontSize: 14,
|
||||
color: "#475569",
|
||||
marginBottom: 20,
|
||||
lineHeight: 20,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
progressContainer: { marginBottom: 24 },
|
||||
progressBar: {
|
||||
statsRow: {
|
||||
flexDirection: "row",
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: "#e2e8f0",
|
||||
overflow: "hidden",
|
||||
marginBottom: 6,
|
||||
justifyContent: "space-around",
|
||||
marginBottom: 20,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
amount: { fontSize: 13, color: "#64748b" },
|
||||
statsRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20 },
|
||||
statBox: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
backgroundColor: "#fff",
|
||||
marginHorizontal: 4,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
alignItems: "center",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.05,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowRadius: 6,
|
||||
},
|
||||
statValue: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: '#1e293b',
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
color: '#64748b',
|
||||
marginTop: 4,
|
||||
},
|
||||
membersHeader: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 15,
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#0f172a",
|
||||
},
|
||||
statusBadge: {
|
||||
backgroundColor: colors.pastelOrange,
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
elevation: 2,
|
||||
},
|
||||
statValue: { fontSize: 18, fontWeight: "700", color: "#1e293b" },
|
||||
statLabel: { fontSize: 12, color: "#64748b", marginTop: 4 },
|
||||
sectionTitle: { fontSize: 16, fontWeight: "600", color: "#0f172a", marginBottom: 12 },
|
||||
membersListContainer: { paddingBottom: 20 },
|
||||
statusText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: colors.Home,
|
||||
},
|
||||
memberCard: {
|
||||
backgroundColor: "#fff",
|
||||
padding: 16,
|
||||
flexDirection: "row",
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fff',
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
marginBottom: 10,
|
||||
borderLeftWidth: 5,
|
||||
elevation: 2,
|
||||
marginHorizontal: 15,
|
||||
},
|
||||
memberInfo: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
memberName: {
|
||||
fontSize: 15,
|
||||
fontWeight: "600",
|
||||
color: "#1e293b",
|
||||
},
|
||||
button: {
|
||||
backgroundColor: colors.pastelOrange,
|
||||
marginHorizontal: 15,
|
||||
marginTop: 20,
|
||||
marginBottom: 30,
|
||||
elevation: 5,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 10,
|
||||
alignItems: "center",
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#000",
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 50,
|
||||
},
|
||||
memberName: { fontSize: 15, fontWeight: "600", color: "#1e293b", marginBottom: 4 },
|
||||
memberDetail: { fontSize: 13, color: "#475569" },
|
||||
actions: { marginTop: 16 },
|
||||
button: { backgroundColor: colors.Home, paddingVertical: 14, borderRadius: 10, alignItems: "center", marginBottom: 12 },
|
||||
buttonText: { fontSize: 15, fontWeight: "600", color: "#fff" },
|
||||
});
|
||||
|
||||
@@ -127,7 +127,8 @@ const ManagementScreen: FC = () => {
|
||||
case "My Groups":
|
||||
return <FlatList data={myGroups} renderItem={({ item }) => <MyGroupCard item={item} onManage={handleManageGroup} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't created any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||
case "Joined Groups":
|
||||
return <FlatList data={joinedGroups} renderItem={({ item }) => <JoinedGroupCard item={item} onViewPress={() => handleViewGroup(item)} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||
return <FlatList data={joinedGroups} renderItem={({ item }) => <JoinedGroupCard item={item} onViewPress={() => {
|
||||
handleViewGroup(item)}} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||
case "Requests":
|
||||
return <FlatList data={requests} renderItem={({ item }) => <RequestCard item={item} onApprove={handleApprove} onDecline={handleDecline} />} keyExtractor={(item) => item.requestId} ListEmptyComponent={<Text style={styles.emptyText}>No pending join requests.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||
case "Your Requests":
|
||||
|
||||
Reference in New Issue
Block a user