diff --git a/src/components/JoinedGroupCard.tsx b/src/components/JoinedGroupCard.tsx index dcb9768..8cd16c4 100644 --- a/src/components/JoinedGroupCard.tsx +++ b/src/components/JoinedGroupCard.tsx @@ -1,8 +1,9 @@ // src/components/JoinedGroupCard.tsx import React, { FC } from 'react'; -import { View, Text, StyleSheet } from 'react-native'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import colors from './colors'; +import { useNavigation } from '@react-navigation/native'; // Update the type to match the full data structure interface JoinedGroup { @@ -62,9 +63,10 @@ const getStatusStyle = (status: JoinedGroup['status']) => { export const JoinedGroupCard: FC = ({ item }) => { const progress = (item.currentParticipants / item.maxParticipants) * 100; const statusStyle = getStatusStyle(item.status); + const navigation = useNavigation() return ( - + navigation.navigate("GroupDetails", {item, color:colors.Settings})}> {/* Header */} {item.groupName} @@ -103,7 +105,7 @@ export const JoinedGroupCard: FC = ({ item }) => { {formatDate(item.startDate)} - {formatDate(item.endDate)} - + ); }; diff --git a/src/components/colors.ts b/src/components/colors.ts index 7431c86..0abc115 100644 --- a/src/components/colors.ts +++ b/src/components/colors.ts @@ -10,12 +10,13 @@ const colors = { Profile: "#2D9CDB", // Deep sky blue Group: "#27AE60", // Green for freshness Settings: "#F39C12", // Amber for warmth + // common text + UI colors textDark: "#212121", textLight: "#555555", white: "#FFFFFF", black: "#000000", - primary: "#a57ae9ff", + primary: "#711ff7ff", }; export default colors; diff --git a/src/components/common/CustomModal.tsx b/src/components/common/CustomModal.tsx new file mode 100644 index 0000000..c6b2f52 --- /dev/null +++ b/src/components/common/CustomModal.tsx @@ -0,0 +1,158 @@ +import React, { useRef, useEffect } from "react"; +import { + View, + Text, + StyleSheet, + Modal, + TouchableOpacity, + Animated, + Dimensions, +} from "react-native"; +import colors from "../colors"; + +const { height } = Dimensions.get("window"); + +type CustomModalProps = { + visible: boolean; + onClose: () => void; + title?: string; + children?: React.ReactNode; + confirmText?: string; + cancelText?: string; + onConfirm?: () => void; + confirmButton?:string +}; + +const CustomModal: React.FC = ({ + visible, + onClose, + title, + children, + confirmText = "Confirm", + cancelText = "Cancel", + onConfirm, + confirmButton = colors.primary +}) => { + const slideAnim = useRef(new Animated.Value(height)).current; // starts off screen + const fadeAnim = useRef(new Animated.Value(0)).current; + + useEffect(() => { + if (visible) { + Animated.parallel([ + Animated.timing(slideAnim, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 1, + duration: 300, + useNativeDriver: true, + }), + ]).start(); + } else { + Animated.parallel([ + Animated.timing(slideAnim, { + toValue: height, + duration: 250, + useNativeDriver: true, + }), + Animated.timing(fadeAnim, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }), + ]).start(); + } + }, [visible]); + + return ( + + {/* Overlay */} + + {/* Modal Box */} + + {/* Header */} + { + title && + + {title} + } + {/* Body */} + {children} + + {/* Footer Actions */} + + + {cancelText} + + + + {confirmText} + + + + + + ); +}; + +export default CustomModal; + +const styles = StyleSheet.create({ + overlay: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.4)", + justifyContent: "flex-end", + }, + modalBox: { + backgroundColor: "#fff", + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + padding: 20, + elevation: 10, + }, + title: { + fontSize: 18, + fontWeight: "700", + color: "#1f2937", + marginBottom: 12, + }, + content: { + marginBottom: 20, + }, + actions: { + flexDirection: "row", + justifyContent: "flex-end", + marginTop: 10, + }, + button: { + paddingVertical: 10, + paddingHorizontal: 18, + borderRadius: 10, + marginLeft: 10, + }, + cancelButton: { + backgroundColor: "#e5e7eb", + }, + + cancelText: { + color: "#374151", + fontWeight: "500", + }, + confirmText: { + color: "#fff", + fontWeight: "600", + }, +}); diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index 87c9b6d..236bc45 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -57,5 +57,6 @@ const styles = StyleSheet.create({ headerTitle: { fontSize: 18, fontWeight: "600", + textTransform:'uppercase' }, }); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 25d7039..b55d9d9 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -27,7 +27,7 @@ export type RootStackParamList = { GroupCreation: undefined; GroupReview: undefined; EditGroup: { groupId: string | number }; - GroupDetails: { groupId: string | number }; + GroupDetails: { item:object }; BecomeOrg: undefined; YourContacts: undefined; Transaction: undefined; diff --git a/src/screens/ContributionScreen.tsx b/src/screens/ContributionScreen.tsx new file mode 100644 index 0000000..cb6691b --- /dev/null +++ b/src/screens/ContributionScreen.tsx @@ -0,0 +1,156 @@ +import React, { useEffect, useRef } from "react"; +import { + View, + Text, + StyleSheet, + Animated, + Easing, +} from "react-native"; +import {Ionicons} from "@react-native-vector-icons/ionicons"; +import colors from "../components/colors"; +import CustomModal from "../components/common/CustomModal"; + +interface ContributionProps { + data: any; + visible: boolean; + onClose: () => void; + onConfirm: () => void; +} + +const ContributionDetails: React.FC = ({ + data, + visible, + onClose, + onConfirm, +}) => { + // Animations + const fadeAnim = useRef(new Animated.Value(0)).current; + const slideAnim = useRef(new Animated.Value(50)).current; + const scaleAnim = useRef(new Animated.Value(0.9)).current; + + useEffect(() => { + if (visible) { + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 1, + duration: 600, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: 0, + duration: 600, + easing: Easing.out(Easing.exp), + useNativeDriver: true, + }), + Animated.spring(scaleAnim, { + toValue: 1, + friction: 6, + useNativeDriver: true, + }), + ]).start(); + } else { + fadeAnim.setValue(0); + slideAnim.setValue(50); + scaleAnim.setValue(0.9); + } + }, [visible]); + + return ( + + {/* Header */} + + + Contribution Summary + + + {/* Contribution Info Card */} + + Base Amount + ₹{data?.baseAmount} + + Total Payable + ₹{data?.totalPayable} + + Due Date + {data?.dueDate} + + Cycle End + {data?.cycleEnd} + + + {/* Extra Details */} + + Cycle Number + {data?.cycleNumber} + + Late Fee Mode + {data?.lateFeeMode} + + Late Fee Value + ₹{data?.lateFeeValue} + + Effective Late Fee + ₹{data?.effectiveLateFee} + + + ); +}; + +export default ContributionDetails; + +const styles = StyleSheet.create({ + header: { + flexDirection: "row", + alignItems: "center", + marginBottom: 24, + }, + headerTitle: { + fontSize: 22, + fontWeight: "700", + color: colors.primary, + marginLeft: 10, + }, + card: { + backgroundColor: "#fff", + padding: 16, + borderRadius: 16, + marginBottom: 16, + shadowColor: "#000", + shadowOpacity: 0.08, + shadowOffset: { width: 0, height: 3 }, + shadowRadius: 6, + elevation: 3, + }, + label: { + fontSize: 13, + color: "#6b7280", + marginTop: 6, + }, + value: { + fontSize: 15, + fontWeight: "500", + color: "#374151", + marginBottom: 6, + }, + valueHighlight: { + fontSize: 18, + fontWeight: "700", + color: colors.primary, + marginBottom: 10, + }, +}); diff --git a/src/screens/home/Home.tsx b/src/screens/home/Home.tsx index 551c946..e783e7e 100644 --- a/src/screens/home/Home.tsx +++ b/src/screens/home/Home.tsx @@ -50,6 +50,8 @@ const Home = () => { return; } const token = credentials.password; + + console.log("token : ", token) const response = await fetch( PROFILE_URL, { diff --git a/src/screens/home/user-home/GroupDetails.tsx b/src/screens/home/user-home/GroupDetails.tsx index 4082231..1f3a893 100644 --- a/src/screens/home/user-home/GroupDetails.tsx +++ b/src/screens/home/user-home/GroupDetails.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useEffect } from "react"; +import React, { useRef, useEffect, useState } from "react"; import { StyleSheet, Text, @@ -6,108 +6,161 @@ import { TouchableOpacity, FlatList, Animated, + ToastAndroid, + Alert, } from "react-native"; -import { useRoute } from "@react-navigation/native"; +import { useRoute, RouteProp } from "@react-navigation/native"; import AppView from "../../../components/AppView"; import colors from "../../../components/colors"; -import Ionicons from "@react-native-vector-icons/ionicons"; +import { Ionicons } from "@react-native-vector-icons/ionicons"; import Header from "../../../components/common/Header"; +import { apiCall } from "../../../utils/ApiHelper"; +import { CONTRIBUTE, GROUP_ALL_MEMBER } from "../../../utils/ApiUrls"; +import ContributionScreen from "../../ContributionScreen"; + +// Define type for a member +interface Member { + id: string; + name: string; + role: "Organizer" | "Member"; + contribution: number; + data: any +} + +// Define route params type + const GroupDetails = () => { - const route = useRoute(); - const { groupId } = route.params as { groupId: string }; + const route = useRoute(); + const { id, groupName, description, contributionAmount, currentParticipants } = route.params; + const [memberData, setMemberData] = useState([]); + const [loading, setLoading] = useState(false); + const [openModal, setOpenModal] = useState(false) + const [payable, setPayable] = useState() + + // Group object const group = { - id: groupId, - name: "Wedding Fund", - collected: 12000, - goal: 20000, - description: - "This group is created to collect contributions for the wedding celebration. Everyone can contribute as per their convenience.", - members: [ - { id: "1", name: "Alice", contribution: 3000, role: "Organizer" }, - { id: "2", name: "Bob", contribution: 2500, role: "Member" }, - { id: "3", name: "Charlie", contribution: 4000, role: "Member" }, - { id: "4", name: "David", contribution: 2500, role: "Member" }, - ], + id: route.params.item?.id, + name: route.params.item?.groupName, + collected: route.params.item?.contributionAmount, + goal: route.params.item?.contributionAmount * route.params.item?.currentParticipants, + description: route.params.item?.description, + status: route.params.item?.status, + members: memberData, }; const progress = Math.min(group.collected / group.goal, 1); // Animation refs - const animatedValues = useRef(group.members.map(() => new Animated.Value(0))).current; + 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 animations = group.members.map((_, i) => - Animated.timing(animatedValues[i], { - toValue: 1, - duration: 400, - delay: i * 100, - useNativeDriver: true, - }) - ); - Animated.stagger(100, animations).start(); - }, []); + 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); + } + }; - const renderMember = ({ item, index }: { item: typeof group.members[0]; index: number }) => { - return ( - - - - - - {item.name} {item.role === "Organizer" ? "(Organizer)" : "(Member)"} - - Contribution: ₹{item.contribution} - + handleGroupDetails(); + }, [id]); + const handleContribution = async () => { + try { + const res = await apiCall(`${CONTRIBUTE}${group.id}/calculate-contribution`, 'GET') as any + setPayable(res.data) + console.log("contribution res : ", res) + } catch (error) { + console.log("contribution error : ", error) + } + } + + // useEffect(()=>{ + // if(group?.id) + // handleContribution() + // },[]) + + const confirmPayment = async () => { + try { + + } catch (error) { + + } + } + const renderMember = ({ item, index }: { item: any; index: number }) => ( + + + + + + {item.firstName} {item.lastName} {item.role === "Organizer" ? "(Organizer)" : "(Member)"} + + Mobile: {item.mobile} - - - ); - }; + + + ); return ( - {/* Group Title */} -
- {/* {group.name} */} +
{group.description} {/* Progress Bar */} - + - - ₹{group.collected} / ₹{group.goal} - + ₹{group.collected} / ₹{group.goal} {/* Stats */} - {group.members.length} + {group.members.length ?? []} Members @@ -121,42 +174,53 @@ const GroupDetails = () => { {/* Members List */} - Members + + Members + {group.status} + + item.id} - renderItem={({ item, index }) => renderMember({ item, index })} + renderItem={renderMember} contentContainerStyle={styles.membersListContainer} /> {/* Actions */} - - - Contribute - - + { + group.status === 'ACTIVE' && + + + { + setOpenModal(true) + handleContribution() + }} + style={[styles.button, { backgroundColor: route.params?.color ? route.params?.color : colors.Home }]}> + Contribute + + + } + { + openModal && setOpenModal(!openModal)} onConfirm={() => { + confirmPayment() + setOpenModal(false) + }} /> + } ); }; export default GroupDetails; +// Styles remain mostly unchanged const styles = StyleSheet.create({ - groupName: { - fontSize: 22, - fontWeight: "700", - color: "#0f172a", - marginBottom: 6, - }, description: { fontSize: 14, color: "#475569", marginBottom: 20, lineHeight: 20, }, - progressContainer: { - marginBottom: 24, - }, + progressContainer: { marginBottom: 24 }, progressBar: { flexDirection: "row", height: 8, @@ -165,18 +229,8 @@ const styles = StyleSheet.create({ overflow: "hidden", marginBottom: 6, }, - progressFill: { - backgroundColor: colors.Home, - }, - amount: { - fontSize: 13, - color: "#64748b", - }, - statsRow: { - flexDirection: "row", - justifyContent: "space-between", - marginBottom: 20, - }, + amount: { fontSize: 13, color: "#64748b" }, + statsRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20 }, statBox: { flex: 1, backgroundColor: "#fff", @@ -190,61 +244,20 @@ const styles = StyleSheet.create({ shadowRadius: 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, - }, + 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 }, memberCard: { backgroundColor: "#fff", padding: 16, borderRadius: 12, marginBottom: 10, - // shadowColor: "#000", - // shadowOpacity: 0.05, - // shadowOffset: { width: 0, height: 2 }, - // shadowRadius: 4, - // elevation: 2, - borderLeftWidth:5, - borderColor:colors.primary - }, - 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", + borderLeftWidth: 5, }, + 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/home/user-home/UserHome.tsx b/src/screens/home/user-home/UserHome.tsx index 5c4ad2f..9e111f0 100644 --- a/src/screens/home/user-home/UserHome.tsx +++ b/src/screens/home/user-home/UserHome.tsx @@ -6,6 +6,7 @@ import { FlatList, TouchableOpacity, Alert, + RefreshControl, } from "react-native"; import { useNavigation } from "@react-navigation/native"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; @@ -34,13 +35,25 @@ const UserHome = () => { const navigation = useNavigation(); const [groups, setGroups] = useState([]); const [loading, setLoading] = useState(false) + const [refreshing, setRefreshing] = useState(false) - const totalGroups = groups.length; - const totalContribution = groups.reduce( + + const activeGroups = groups.filter((g) => g.status === "ACTIVE"); + + const totalGroups = activeGroups.length; + + const totalContribution = activeGroups.reduce( (sum, g) => sum + g.contributionAmount, 0 ); + + + const onRefresh = async () => { + setRefreshing(true) + await handleGroup() + setRefreshing(false) + } const handleGroup = async () => { try { setLoading(true) @@ -70,7 +83,7 @@ const UserHome = () => { } catch (error) { console.error("❌ Error fetching groups:", error); Alert.alert("Error", "Could not connect to server"); - }finally{ + } finally { setLoading(false) } }; @@ -83,11 +96,10 @@ const UserHome = () => { return ( navigation.navigate("GroupDetails", { groupId: item.id })} + onPress={() => navigation.navigate("GroupDetails", { item })} > {item.groupName} - {item.description} - + {item.description} { ₹{totalContribution} - Total Contribution + Cycle Contribution {/* Groups List */} Your Groups status.status === "ACTIVE")} keyExtractor={(item) => item.id} renderItem={renderItem} contentContainerStyle={{ paddingBottom: 20 }} + onRefresh={onRefresh} + refreshing={refreshing} /> ); @@ -178,6 +192,8 @@ const styles = StyleSheet.create({ shadowOffset: { width: 0, height: 2 }, shadowRadius: 6, elevation: 2, + borderLeftWidth: 5, + borderColor: colors.Home }, groupName: { fontSize: 16, diff --git a/src/utils/ApiHelper.tsx b/src/utils/ApiHelper.tsx index 84552fa..c398504 100644 --- a/src/utils/ApiHelper.tsx +++ b/src/utils/ApiHelper.tsx @@ -13,11 +13,11 @@ export async function apiCall( method: "GET" | "POST" | "PUT" | "DELETE" = "GET", data?: any, config?: AxiosRequestConfig -): Promise { +): Promise { try { // Get saved credentials from Keychain const creds = await Keychain.getGenericPassword(); - console.log("hihihi", creds); + // console.log("hihihi", creds); const headers = { "Content-Type": "application/json", ...(creds ? { Authorization: `Bearer ${creds.password}` } : {}), diff --git a/src/utils/ApiUrls.tsx b/src/utils/ApiUrls.tsx index 7205ace..32ef573 100644 --- a/src/utils/ApiUrls.tsx +++ b/src/utils/ApiUrls.tsx @@ -18,3 +18,4 @@ export const PUT_USER_STATUS = `${BASE_URL}user/profile/status` export const USER_REQUEST = `${BASE_URL}fund/my-requests` export const GROUP_ALL_MEMBER = `${BASE_URL}fund/group/all/member` export const UPDATE_BANK_URL = `${BASE_URL}user/bank/update`; +export const CONTRIBUTE = `${BASE_URL}payments/groups/`;