diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx
index c4ad6cf..5c3a0e9 100644
--- a/src/components/common/Header.tsx
+++ b/src/components/common/Header.tsx
@@ -46,6 +46,7 @@ const styles = StyleSheet.create({
alignItems: "center",
justifyContent: "space-between",
paddingVertical: 12,
+ paddingHorizontal:12
},
headerTitle: {
fontSize: 18,
diff --git a/src/screens/home/user-home/CycleDashboard.tsx b/src/screens/home/user-home/CycleDashboard.tsx
new file mode 100644
index 0000000..0f4c8e9
--- /dev/null
+++ b/src/screens/home/user-home/CycleDashboard.tsx
@@ -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 (
+
+
+
+ Cycle {item.cycleNumber}
+
+ {formatDate(item.cycleStart)} - {formatDate(item.cycleEnd)}
+
+
+
+
+
+ {isExpanded && (
+
+ {/* Withdrawal Info */}
+ {item.withdrawal && (
+
+ Withdrawal
+
+
+
+ {item.withdrawal.userName}
+ {formatCurrency(item.withdrawal.totalAmount)}
+
+ {formatDate(item.withdrawal.paidDate)}
+
+
+ )}
+
+ {/* Contributions List */}
+
+ Contributions ({item.paidParticipants}/{item.totalParticipants})
+ {item.contributions.map(contribution => (
+
+
+
+ {contribution.userName}
+ {formatDate(contribution.paidDate)}
+
+ {formatCurrency(contribution.totalAmount)}
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+
+// --- Main Dashboard Component ---
+
+const CycleDashboard = ({ data }: { data: CycleDashboardData }) => {
+ const [expandedCycle, setExpandedCycle] = useState(data.currentCycle);
+
+ const handleToggle = (cycleNumber: number) => {
+ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
+ setExpandedCycle(expandedCycle === cycleNumber ? null : cycleNumber);
+ };
+
+ const renderHeader = () => (
+
+ {data.groupName}
+
+
+ Total Collected
+ {formatCurrency(data.totalCollected)}
+
+
+ Status
+ {data.status}
+
+
+ Cycles
+ {data.currentCycle}/{data.totalCycles}
+
+
+
+ );
+
+ return (
+ item.cycleNumber.toString()}
+ renderItem={({ item }) => (
+ 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',
+ },
+});
+
diff --git a/src/screens/home/user-home/GroupDetails.tsx b/src/screens/home/user-home/GroupDetails.tsx
index e17e430..f93a12c 100644
--- a/src/screens/home/user-home/GroupDetails.tsx
+++ b/src/screens/home/user-home/GroupDetails.tsx
@@ -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 (
+
+ setOpen(true)} style={styles.dropdownHeader}>
+ {selectedLabel}
+
+
+ setOpen(false)}
+ >
+ setOpen(false)}>
+
+
+ {items.map(item => (
+ {
+ onValueChange(item.value);
+ setOpen(false);
+ }}
+ style={[
+ styles.dropdownItem,
+ item.value === selectedValue && styles.selectedItem
+ ]}
+ >
+ {item.label}
+
+ ))}
+
+
+
+
+
+ );
+};
const GroupDetails = () => {
const route = useRoute();
- const { id, groupName, description, contributionAmount, currentParticipants } = route.params;
-
- const [memberData, setMemberData] = useState([]);
+ 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();
+ const [refreshing, setRefreshing] = useState(false);
+ const [cycleDashboardData, setCycleDashboardData] = useState();
+ 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([]);
-
- // 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 }) => (
-
-
-
-
-
- {item.firstName} {item.lastName} {item.role === "Organizer" ? "(Organizer)" : "(Member)"}
-
- Mobile: {item.mobile}
-
-
-
- );
+ const selectedCycleData = cycleDashboardData?.cycles.find(c => c.cycleNumber === selectedCycleNumber);
return (
-
-
- {group.description}
+
+
+ }
+ contentContainerStyle={styles.scrollContent}
+ >
+
+ {group.description}
- {/* Progress Bar */}
-
-
-
-
+
+
+ {group.members.length ?? 0}
+ Members
+
+
+ ₹{group.collected}
+ Contribution
+
+
+ ₹{group.goal}
+ Total Amount
+
- ₹{group.collected} / ₹{group.goal}
-
- {/* Stats */}
-
-
- {group.members.length ?? []}
- Members
+ {cycleItems.length > 0 && (
+
+ )}
+
+ {selectedCycleData && cycleDashboardData && (
+
+ )}
+
+
+ Members
+
+ {group.status}
+
-
- ₹{group.collected}
- Collected
-
-
- ₹{group.goal}
- Goal
-
-
- {/* Members List */}
-
- Members
- {group.status}
-
+ {memberData.map((member:any) => (
+
+
+
+
+ {member.firstName} {member.role === "Organizer" ? "(Organizer)" : ""}
+
+
+
+ ))}
- item.id}
- renderItem={renderMember}
- contentContainerStyle={styles.membersListContainer}
- />
-
- {/* Actions */}
- {
- group.status === 'ACTIVE' &&
-
-
- {
- 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 && (
+ setOpenModal(true)} style={styles.button}>
Contribute
-
- }
- {
- openModal && setOpenModal(!openModal)} onConfirm={() => {
- confirmPayment()
- setOpenModal(false)
- }} />
- }
+ )}
+
+
+ {openModal && (
+ setOpenModal(false)}
+ onConfirm={() => {
+ confirmPayment();
+ setOpenModal(false);
+ }}
+ />
+ )}
);
};
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" },
});
diff --git a/src/screens/management/ManagementScreen.tsx b/src/screens/management/ManagementScreen.tsx
index d394f08..e5634de 100644
--- a/src/screens/management/ManagementScreen.tsx
+++ b/src/screens/management/ManagementScreen.tsx
@@ -127,7 +127,8 @@ const ManagementScreen: FC = () => {
case "My Groups":
return } keyExtractor={(item) => item.id} ListEmptyComponent={You haven't created any groups yet.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
case "Joined Groups":
- return handleViewGroup(item)} />} keyExtractor={(item) => item.id} ListEmptyComponent={You haven't joined any groups yet.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
+ return {
+ handleViewGroup(item)}} />} keyExtractor={(item) => item.id} ListEmptyComponent={You haven't joined any groups yet.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
case "Requests":
return } keyExtractor={(item) => item.requestId} ListEmptyComponent={No pending join requests.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
case "Your Requests":