diff --git a/src/components/JoinedGroupCard.tsx b/src/components/JoinedGroupCard.tsx index 6ef1d79..1ce03ca 100644 --- a/src/components/JoinedGroupCard.tsx +++ b/src/components/JoinedGroupCard.tsx @@ -3,6 +3,7 @@ import React, { FC } from 'react'; 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 { @@ -63,9 +64,10 @@ const getStatusStyle = (status: JoinedGroup['status']) => { export const JoinedGroupCard: FC = ({ item, onViewPress }) => { 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} @@ -107,7 +109,7 @@ export const JoinedGroupCard: FC = ({ item, onViewPress }) View - + ); }; 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 03c35ad..c8612e9 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -30,7 +30,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/navigation/BottomTabs.tsx b/src/navigation/BottomTabs.tsx index 5fe70ea..30fabce 100644 --- a/src/navigation/BottomTabs.tsx +++ b/src/navigation/BottomTabs.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; +import { BottomTabNavigationProp, createBottomTabNavigator } from "@react-navigation/bottom-tabs"; import { View, Text, StyleSheet, Platform } from "react-native"; import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons"; import GroupScreen from "../screens/GroupScreen"; @@ -9,7 +9,6 @@ import colors from "../components/colors"; import Setting from "../screens/setting/Setting"; import ManagementScreen from "../screens/management/ManagementScreen"; -const Tab = createBottomTabNavigator(); const TAB_CONFIG = { activeColor: "#000", @@ -29,26 +28,24 @@ const activeTabColors: { [key: string]: string } = { // Icon/Text colors on active tabs const activeIconColors: { [key: string]: string } = { - Home: "#333", - Profile: "#333", - Group: "#333", - Management: "#333", + Home: "#333", + Profile: "#333", + Group: "#333", + Management: "#333", } -function HomeScreen() { - return ( - 🏠 Home - ); -} +export type RootTabParamList = { + Home: undefined; + Profile: undefined; + Group: undefined; + Management: { actionTab?: "My Groups" | "Joined Groups" | "Requests" | "Your Requests" }; +}; +// Define the navigation prop type for screens within the tab navigator +export type TabNavigationProp = BottomTabNavigationProp; +const Tab = createBottomTabNavigator(); -function SettingsScreen() { - return ( - ⚙️ Settings - ); -} - -export default function BottomTabs() { +const BottomTabs: React.FC = () => { return ( ({ @@ -62,7 +59,7 @@ export default function BottomTabs() { tabBarIcon: ({ focused }) => { let iconName: IoniconsIconName = "home-outline"; - + // Determine icon based on route if (route.name === "Home") iconName = focused ? "home" : "home-outline"; else if (route.name === "Profile") iconName = focused ? "person" : "person-outline"; @@ -151,3 +148,4 @@ const styles = StyleSheet.create({ shadowRadius: 5, }, }); +export default BottomTabs \ No newline at end of file diff --git a/src/navigation/types.ts b/src/navigation/types.ts new file mode 100644 index 0000000..26debb7 --- /dev/null +++ b/src/navigation/types.ts @@ -0,0 +1,42 @@ +// navigation/types.ts + +import { NavigatorScreenParams } from '@react-navigation/native'; +import { StackScreenProps } from '@react-navigation/stack'; + +// Type for the parameters passed to each screen in the Bottom Tab Navigator +export type RootTabParamList = { + Home: undefined; + Profile: undefined; + Group: undefined; + Management: { actionTab?: "My Groups" | "Joined Groups" | "Requests" | "Your Requests" }; +}; + +// Type for the parameters passed to each screen in the main Stack Navigator + +export type RootStackParamList = { + Splash: undefined; + Login: undefined; + OtpVarify: { mobile: string }; + Home: undefined; + HomeTabs: undefined; + OrganizerRegister: undefined; + Profile: undefined; + GroupCreation: undefined; + GroupReview: undefined; + EditGroup: { groupId: string | number }; + GroupDetails: { item:object }; + BecomeOrg: undefined; + YourContacts: undefined; + Transaction: undefined; + EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null }; + ManageJoinedGroup: { groupId: string | number }; + Invite: { groupId: string | number; code: string | number }; + ManagementTabs:any +}; + + +// Helper type for screens within the main stack navigator +export type AppStackScreenProps = StackScreenProps< + RootStackParamList, + T +>; 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/InviteScreen.tsx b/src/screens/InviteScreen.tsx index 01e6303..fab9e6c 100644 --- a/src/screens/InviteScreen.tsx +++ b/src/screens/InviteScreen.tsx @@ -76,7 +76,7 @@ const InviteScreen: FC = ({ route }) => { try { // Replace with your actual API call to join the group // For now, it just shows an alert. - // await apiCall(`/api/fund/group/join`, 'POST', { groupId: groupDetails.id }); + 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.'); @@ -99,11 +99,12 @@ const InviteScreen: FC = ({ route }) => { {/* Group Info Card */} - Group Details + Group Details + {/* Participants Card */} @@ -143,7 +144,7 @@ const InviteScreen: FC = ({ route }) => { }; // Helper component for displaying rows -const InfoRow = ({ label, value }: { label: string; value: string }) => ( +const InfoRow = ({ label, value }: { label: string; value: string | number }) => ( {label} {value} diff --git a/src/screens/home/Home.tsx b/src/screens/home/Home.tsx index 551c946..1750d24 100644 --- a/src/screens/home/Home.tsx +++ b/src/screens/home/Home.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { StyleSheet, Text, @@ -8,86 +8,70 @@ import { Platform, Image, Alert, + FlatList, + ListRenderItem, // Import ListRenderItem for better typing } from "react-native"; import AppView from "../../components/AppView"; -import UserProfileStatus from "./UserProfileStatus"; import colors from "../../components/colors"; -import GroupsTabs from "./GroupsTabs"; import { useDispatch, useSelector } from "react-redux"; -import { RouteProp, useFocusEffect, useNavigation, useRoute } from "@react-navigation/native"; -import UserHome from "./user-home/UserHome"; +import { useNavigation } from "@react-navigation/native"; import { PROFILE_URL } from "../../utils/ApiUrls"; -import *as Keychain from "react-native-keychain" +import * as Keychain from "react-native-keychain"; import { setUserData } from "../../redux/userSlice"; -// Enable animation for Android +import { TabNavigationProp } from "../../navigation/BottomTabs"; + +// Enable layout animation for Android if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } -type RootStackParamList = { - Home: { refreshProfile: boolean; role: string }; -}; - -type UserProfile = { +// --- TYPE DEFINITIONS --- +interface ActionItem { id: string; - userId: number; - firstName: string | null; - lastName: string | null; - imageBlob: string | null; - role?: string; + title: string; + action: string; + tab?: "My Groups" | "Joined Groups" | "Requests" | "Your Requests"; + imageUri: string; } -const Home = () => { - const user = useSelector((state: any) => state.user); - const navigation = useNavigation(); - const dispatch = useDispatch() - const [apiUser, setApiUser] = useState(null) +// Data for the grid items +const organizerActions: ActionItem[] = [ + { id: "1", title: "My Groups", action: "NAVIGATE_MANAGEMENT", tab: "My Groups", imageUri: "https://via.placeholder.com/60" }, + { id: "2", title: "Joined Groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", imageUri: "https://via.placeholder.com/60" }, + { id: "3", title: "Requests", action: "NAVIGATE_MANAGEMENT", tab: "Requests", imageUri: "https://via.placeholder.com/60" }, + +]; + +const userActions: ActionItem[] = [ + { id: "1", title: "Joined Groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", imageUri: "https://via.placeholder.com/60" }, + { id: "2", title: "Your Requests", action: "NAVIGATE_MANAGEMENT", tab: "Your Requests", imageUri: "https://via.placeholder.com/60" }, + { id: "3", title: "Action 3", action: "ACTION_3", imageUri: "https://via.placeholder.com/60" }, + { id: "4", title: "Action 4", action: "ACTION_4", imageUri: "https://via.placeholder.com/60" }, +]; + +const Home: React.FC = () => { + const user = useSelector((state: any) => state.user); + const navigation = useNavigation(); + const dispatch = useDispatch(); + + // --- FIXED: Conditional logic moved inside the component --- + const actions = user.role === 'ORGANIZER' ? organizerActions : userActions; const fetchUserProfile = async () => { try { const credentials = await Keychain.getGenericPassword(); - if (!credentials) { - return; - } + if (!credentials) return; const token = credentials.password; - const response = await fetch( - PROFILE_URL, - { - method: "GET", - headers: { - Accept: "*/*", - Authorization: `Bearer ${token}`, - }, - } - ); + + const response = await fetch(PROFILE_URL, { + method: "GET", + headers: { Accept: "*/*", Authorization: `Bearer ${token}` }, + }); const result = await response.json(); - console.log("👤 Profile Response:", result); if (result.status === "success") { - setApiUser(result.data); - dispatch( - setUserData({ - id: result.data.id, - userId: result.data.userId, - username: result.data.username, - firstName: result.data.firstName, - lastName: result.data.lastName, - gender: result.data.gender, - mobile: result.data.mobile, - email: result.data.email, - address: result.data.address, - bankAccountNumber: result.data.bankAccountNumber, - bankName: result.data.bankName, - branchCode: result.data.branchCode, - profilePicture: result.data.filePath, - filePath: result.data.filePath, - imageBlob: result.data.imageBlob, - role: result.data.role, - organizerId: result.data.organizerId, - organizerStatus: result.data.organizerStatus, - profileStatus: true, - }) - ); + // No need for apiUser state, just dispatch to Redux + dispatch(setUserData({ ...result.data, profileStatus: true })); } } catch (error) { console.error("❌ Error fetching profile:", error); @@ -98,27 +82,45 @@ const Home = () => { fetchUserProfile(); }, []); + // --- REFACTORED: Simplified action handler --- + const handleActionPress = (item: ActionItem) => { + if (item.action === "NAVIGATE_MANAGEMENT" && item.tab) { + navigation.navigate('Management', { actionTab: item.tab }); + } else if (item.action === "ACTION_3") { + Alert.alert("Action 3 pressed"); + } else if (item.action === "ACTION_4") { + Alert.alert("Action 4 pressed"); + } else { + Alert.alert("Unknown action", item.title); + } + }; + + // Render function for each grid item + const renderGridItem: ListRenderItem = ({ item }) => ( + handleActionPress(item)} + > + {/* FIXED: 'imageUri' was missing from your data objects */} + + {item.title} + + ); + return ( - {/* Header */} - Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}👋 - navigation.navigate("Profile" as never)}> - - - + Home - {/* Status */} - - {/* */} - {user?.role === "USER" || apiUser?.role === "USER" ? : - } + + item.id} + numColumns={2} + columnWrapperStyle={styles.row} + contentContainerStyle={{ paddingHorizontal: 8 }} + /> ); }; @@ -127,13 +129,48 @@ export default Home; const styles = StyleSheet.create({ container: { - paddingBottom: '25%', + flex: 1, + backgroundColor: '#F4F6F8' }, header: { - flexDirection: "row", - justifyContent: "space-between", + justifyContent: "center", alignItems: "center", - marginBottom: 12, + paddingVertical: 12, + marginBottom: 16, }, - greeting: { fontSize: 22, fontWeight: "600", color: colors.Home }, -}); \ No newline at end of file + headerTitle: { + fontSize: 22, + fontWeight: "600", + color: colors.Home, + }, + row: { + flex: 1, + justifyContent: "space-around", + }, + gridButton: { + flex: 1, + margin: 8, + backgroundColor: "#FFFFFF", + borderRadius: 12, + padding: 16, + alignItems: "center", + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 5, + elevation: 4, + }, + gridIcon: { + width: 60, + height: 60, + marginBottom: 12, + borderRadius: 8, + backgroundColor: '#E0E0E0', + }, + gridLabel: { + fontSize: 16, + fontWeight: "500", + color: colors.Home, + textAlign: "center", + }, +}); 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/screens/management/EditGroupScreen.tsx b/src/screens/management/EditGroupScreen.tsx index abfaa0b..617fc9d 100644 --- a/src/screens/management/EditGroupScreen.tsx +++ b/src/screens/management/EditGroupScreen.tsx @@ -338,6 +338,7 @@ const EditGroupScreen: React.FC = ({ route }) => { {renderCard('Financial Rules', [ { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` }, { field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` }, + { field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` }, { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` }, ], 'financialRules')} @@ -403,7 +404,6 @@ const styles = StyleSheet.create({ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 8, - backgroundColor: 'white', // Changed for a cleaner header look borderBottomWidth: 1, borderBottomColor: '#E5E7EB', }, @@ -436,7 +436,7 @@ const styles = StyleSheet.create({ saveButton: { backgroundColor: colors.pastelOrange }, cancelButton: { backgroundColor: '#E5E7EB' }, inviteButton: { backgroundColor: colors.primary }, - buttonText: { color: '#000000ff', fontSize: 15, fontWeight: '600' }, + buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' }, // --- FLOATING BUTTON STYLES --- floatingButtonContainer: { diff --git a/src/screens/management/ManagementScreen.tsx b/src/screens/management/ManagementScreen.tsx index 0eeaba0..c3a6425 100644 --- a/src/screens/management/ManagementScreen.tsx +++ b/src/screens/management/ManagementScreen.tsx @@ -1,6 +1,6 @@ // src/screens/ManagementScreen.tsx -import React, { useState, useCallback, FC } from "react"; +import React, { useState, useCallback, FC, useEffect } from "react"; import { View, Text, @@ -9,69 +9,74 @@ import { TouchableOpacity, ActivityIndicator, Alert, - ListRenderItemInfo, } from "react-native"; -import { useNavigation, useFocusEffect } from "@react-navigation/native"; +import { useNavigation, useFocusEffect, useRoute, RouteProp } from "@react-navigation/native"; import { StackNavigationProp } from "@react-navigation/stack"; -import { RootStackParamList } from "../../navigation/AppNavigator"; -import { APPROVE_REQUEST_URL, BASE_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, JOIN_GROUP_URL, MY_GROUPS_URL, REQUESTS_URL, USER_REQUEST } from "../../utils/ApiUrls"; +import { APPROVE_REQUEST_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, MY_GROUPS_URL, REQUESTS_URL, USER_REQUEST } from "../../utils/ApiUrls"; import colors from "../../components/colors"; import AppView from "../../components/AppView"; import { apiCall } from "../../utils/ApiHelper"; -import * as Keychain from "react-native-keychain"; -import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types'; +import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types'; // Adjust import path import { MyGroupCard } from "../../components/MyGroupCard"; import { JoinedGroupCard } from "../../components/JoinedGroupCard"; import { RequestCard } from "../../components/RequestCard"; import { useSelector } from "react-redux"; -import Ionicons from "@react-native-vector-icons/ionicons"; import { UserRequested } from "../../components/UserRequested"; +import { RootTabParamList } from "../../navigation/BottomTabs"; // Adjust import path -type GroupManagementNavProp = StackNavigationProp; +// --- TYPE DEFINITIONS --- +type GroupManagementNavProp = StackNavigationProp; +type ManagementRouteProp = RouteProp; type ActiveTab = "My Groups" | "Joined Groups" | "Requests" | "Your Requests"; const ManagementScreen: FC = () => { - const userRole = useSelector((state: any) => state.user) - const [activeTab, setActiveTab] = useState(userRole.role === "USER" ? "Joined Groups" : "My Groups"); + const user = useSelector((state: any) => state.user); const navigation = useNavigation(); + const route = useRoute(); + + const [activeTab, setActiveTab] = useState(user.role === "USER" ? "Joined Groups" : "My Groups"); const [myGroups, setMyGroups] = useState([]); const [joinedGroups, setJoinedGroups] = useState([]); const [requests, setRequests] = useState([]); + const [userRequest, setUserRequest] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [userRequest, setUserRequest] = useState([]) + + // --- HOOKS --- + + // Effect to set the initial tab from route params + useEffect(() => { + const passedTab = route.params?.actionTab; + if (passedTab) { + const availableTabs = user.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"]; + if (availableTabs.includes(passedTab)) { + setActiveTab(passedTab); + } + } + }, [route.params?.actionTab, user.role]); + + // --- FIXED: fetchData dependency array is corrected --- const fetchData = useCallback(async () => { + // Check for loading state at the start to prevent multiple simultaneous fetches if (loading) return; setLoading(true); setError(null); - try { - if (userRole.role === "ORGANIZER") { - // Fetch all data concurrently for organizers + if (user.role === "ORGANIZER") { const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([ apiCall(MY_GROUPS_URL, "GET") as Promise>, apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>, apiCall(REQUESTS_URL, "GET") as Promise>, ]); - - console.log("My Groups:", myGroupsRes.data); - console.log("Joined Groups:", joinedGroupsRes.data); - console.log("Requests:", requestsRes.data); - setMyGroups(myGroupsRes.data || []); setJoinedGroups(joinedGroupsRes.data || []); setRequests(requestsRes.data || []); } else { - // Fetch only joined groups and user requests for non-organizers const [joinedGroupsRes, userRequestRes] = await Promise.all([ apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>, apiCall(USER_REQUEST, "GET") as Promise>, ]); - - console.log("Joined Groups:", joinedGroupsRes.data); - console.log("User Requests:", userRequestRes.data); - setJoinedGroups(joinedGroupsRes.data || []); setUserRequest(userRequestRes.data || []); } @@ -81,113 +86,64 @@ const ManagementScreen: FC = () => { } finally { setLoading(false); } - }, [userRole, loading]); + // The only external dependency that should trigger a re-fetch is the user's role. + }, [user.role]); // **FIXED**: Removed `loading` from the dependency array - useFocusEffect(useCallback(() => { fetchData(); }, [])); + // --- FIXED: useFocusEffect now has a stable dependency --- + useFocusEffect( + useCallback(() => { + fetchData(); + }, [fetchData]) // This will now only re-run if fetchData (and thus user.role) changes. + ); + + // --- HANDLERS (No changes needed here) --- const handleViewGroup = (item: JoinedGroup) => { - console.log("PPP",item) - navigation.navigate('Invite', { groupId: item.id } as any); - } - - const handleAction = async (url: string, body: object, successMessage: string, failureMessage: string, action: string) => { - // ... (logic remains the same) - console.log(url) + // Assuming you have an 'Invite' screen in your RootTabParamList or another navigator + // navigation.navigate('Invite' as any, { groupId: item.id }); + }; + + const handleAction = async (url: string, successMessage: string, failureMessage: string) => { try { - setLoading(true); // Optionally show a loading state during the action - // Make the POST request with an empty body + // No need to set loading here if the whole screen is already loading const res = await apiCall(url, "PUT", {}) as ApiResponse | null; - if (res && res.statusCode === 200) { - // Use the success message from the API response - Alert.alert("Success", res.message || `Request ${action}d successfully.`); - fetchData(); // Refresh the list of requests + Alert.alert("Success", res.message || successMessage); + fetchData(); // Refresh data after action } else { throw new Error(res?.message || failureMessage); } } catch (err: any) { Alert.alert("Error", err.message || "An unexpected error occurred."); - } finally { - setLoading(false); } }; - const handleApprove = (req: JoinRequest) => handleAction(APPROVE_REQUEST_URL + req.requestId + "/approve", { requestId: req.requestId }, `${req.userName} has been added.`, "Failed to approve request.", "Approve"); - const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL + req.requestId + "/reject?reason=/" + " ", { requestId: req.requestId }, `Request from ${req.userName} has been declined.`, "Failed to decline request.", "Decline"); + const handleApprove = (req: JoinRequest) => handleAction(APPROVE_REQUEST_URL + req.requestId + "/approve", `${req.userName} has been added.`, "Failed to approve request."); + const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL + req.requestId + "/reject?reason=/", `Request from ${req.userName} has been declined.`, "Failed to decline request."); const handleManageGroup = (group: MyGroup) => navigation.navigate('ManagementTabs', { groupId: group.id }); + // --- RENDER LOGIC --- const renderContent = () => { - if (error) { - return {error}; + if (error) return {error}; + // Improved loading logic to avoid hiding existing data during a refresh + if (loading && myGroups.length === 0 && joinedGroups.length === 0 && requests.length === 0) { + return ; } switch (activeTab) { case "My Groups": - if (loading) { - return ; - } - return ( - ) => } - keyExtractor={(item) => item.id} - ListEmptyComponent={You haven't created any groups yet.} - onRefresh={fetchData} - refreshing={loading} // This handles the "continuing loading" on pull-to-refresh - contentContainerStyle={{ paddingBottom: 100 }} - /> - ); + return } keyExtractor={(item) => item.id} ListEmptyComponent={You haven't created any groups yet.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />; case "Joined Groups": - if (loading && joinedGroups.length === 0) { - return ; - } - return ( - ) => handleViewGroup(item)} />} - keyExtractor={(item) => item.id} - ListEmptyComponent={You haven't joined any groups yet.} - onRefresh={fetchData} - refreshing={loading} // This handles the "continuing loading" on pull-to-refresh - 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": - if (loading && requests.length === 0) { - return ; - } - return ( - ) => } - keyExtractor={(item) => item.requestId} - ListEmptyComponent={No pending join requests.} - onRefresh={fetchData} - refreshing={loading} // This handles the "continuing loading" on pull-to-refresh - contentContainerStyle={{ paddingBottom: 100 }} - /> - ); + return } keyExtractor={(item) => item.requestId} ListEmptyComponent={No pending join requests.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />; case "Your Requests": - if (loading && requests.length === 0) { - return ; - } - return ( - ) => } - keyExtractor={(item) => item.requestId} - ListEmptyComponent={No pending join requests.} - onRefresh={fetchData} - refreshing={loading} // This handles the "continuing loading" on pull-to-refresh - contentContainerStyle={{ paddingBottom: 100 }} - /> - ); + return } keyExtractor={(item) => item.requestId} ListEmptyComponent={You haven't sent any join requests.} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />; default: return null; } }; - const tabs: ActiveTab[] = userRole.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"]; + const tabs: ActiveTab[] = user.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"]; - console.log("user request : ", userRequest) return ( @@ -201,32 +157,6 @@ const ManagementScreen: FC = () => { ))} - { - activeTab === "Requests" && userRole === "ORGANIZER" ? ( - navigation.navigate("YourContacts" as never)} - > - - - ) : null - } - {renderContent()} @@ -234,59 +164,17 @@ const ManagementScreen: FC = () => { }; const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: '#f5f6fa' - }, - header: { - paddingHorizontal: 20, - paddingBottom: 15, - }, - title: { - fontSize: 22, fontWeight: "600", color: colors.Settings - }, - segmentedControl: { - flexDirection: 'row', - backgroundColor: '#e9ecef', - borderRadius: 10, - marginHorizontal: 20, - marginBottom: 20, - padding: 3, - }, - segmentButton: { - flex: 1, - paddingVertical: 12, - alignItems: 'center', - borderRadius: 8, - }, - segmentActive: { - backgroundColor: colors.pastelOrange, // Example color - elevation: 3, - shadowColor: '#000', - shadowOpacity: 0.1, - shadowRadius: 5, - }, - segmentText: { - fontSize: 14, - fontWeight: '600', - color: '#6c757d', - }, - segmentTextActive: { - color: colors.textDark, // Example color - }, - contentArea: { - flex: 1, - paddingHorizontal: 20, - }, - loader: { - marginTop: 50, - }, - emptyText: { - textAlign: 'center', - marginTop: 60, - fontSize: 16, - color: '#6c757d', - }, + container: { flex: 1, backgroundColor: '#f5f6fa' }, + header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 }, + title: { fontSize: 22, fontWeight: "600", color: colors.Settings }, + segmentedControl: { flexDirection: 'row', backgroundColor: '#e9ecef', borderRadius: 10, marginHorizontal: 20, marginBottom: 20, padding: 3 }, + segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 }, + segmentActive: { backgroundColor: colors.pastelOrange, elevation: 3, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 }, + segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' }, + segmentTextActive: { color: colors.textDark }, + contentArea: { flex: 1, paddingHorizontal: 20 }, + loader: { marginTop: 50 }, + emptyText: { textAlign: 'center', marginTop: 60, fontSize: 16, color: '#6c757d' }, }); export default ManagementScreen; diff --git a/src/screens/management/Member.tsx b/src/screens/management/Member.tsx index 55c33ac..1551a13 100644 --- a/src/screens/management/Member.tsx +++ b/src/screens/management/Member.tsx @@ -14,6 +14,7 @@ import { Ionicons } from "@react-native-vector-icons/ionicons"; // Assuming you import { apiCall } from '../../utils/ApiHelper'; // Adjust path to your apiCall utility import AppView from '../../components/AppView'; import colors from '../../components/colors'; +import { useRoute } from '@react-navigation/native'; // --- Type Definitions --- interface Member { @@ -38,8 +39,8 @@ interface ApiResponse { // --- Main Screen Component --- // This assumes the component receives `groupId` from a navigator (e.g., Tab Navigator) -const MemberScreen = () => { - const route = use +const MemberScreen = ({ route }) => { + const { groupId } = route.params; const [members, setMembers] = useState([]); const [loading, setLoading] = useState(true); diff --git a/src/screens/management/Payments.tsx b/src/screens/management/Payments.tsx index 7d261ff..da4f2d8 100644 --- a/src/screens/management/Payments.tsx +++ b/src/screens/management/Payments.tsx @@ -1,12 +1,246 @@ -import { View, Text } from 'react-native' -import React from 'react' - -const Payments = () => { - return ( - - Payments - - ) +import React, { useEffect, useState, useCallback } from 'react'; +import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity } from 'react-native'; +import { useRoute, useNavigation } from '@react-navigation/native'; +import * as Keychain from 'react-native-keychain'; +import { Ionicons } from "@react-native-vector-icons/ionicons"; +import colors from '../../components/colors'; // Assuming you have a colors file +import AppView from '../../components/AppView'; +import { apiCall } from '../../utils/ApiHelper'; +interface ApiResponse { + status: 'success' | 'error'; + data: T; + message?: string; + statusCode: number; } -export default Payments \ No newline at end of file +// Define the shape of your payment details data +interface PaymentDetails { + // Add properties based on your API response, for example: + amount: number; + currency: string; + dueDate: string; + paidBy: string[]; +} +const Payments = () => { + const route = useRoute(); + const navigation = useNavigation(); + const { groupId } = route.params; + + const [paymentDetails, setPaymentDetails] = useState(null); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + +const fetchPaymentDetails = useCallback(async () => { + setLoading(true); + try { + // The apiCall function likely handles tokens internally, so we call it directly. + const response = await apiCall( + `https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/current/payment/details/${groupId}`, + 'GET' + ) as ApiResponse; // Cast the response to the correct type + + console.log("Payment Details Response:", response); + + // Check the response and status property for success + if (response && response.statusCode === 200) { + setPaymentDetails(response.data); + } else { + // Use the message from the response object + Alert.alert("Error", response?.message || "Failed to fetch payment details."); + } + } catch (error: any) { + console.error("Fetch payment details error:", error); + Alert.alert("Error", error.message || "An unexpected error occurred while fetching payment details."); + } finally { + setLoading(false); + setRefreshing(false); + } +}, [groupId]); + + useEffect(() => { + fetchPaymentDetails(); + }, [fetchPaymentDetails]); + + const onRefresh = () => { + setRefreshing(true); + fetchPaymentDetails(); + }; + + const renderParticipant = (participant: any) => ( + + + {participant.firstName} {participant.lastName} + {participant.mobile} + + + + {participant.status} + + + ₹{participant.paidAmount} / ₹{participant.contributionAmount} + + {/* {participant.status === 'PENDING' && ( + + Pay Now + + )} */} + + + ); + + if (loading) { + return ; + } + + if (!paymentDetails) { + return ( + + No payment details found. + + Tap to retry + + + ); + } + + return ( + + } + > + + {paymentDetails.groupName} + + + + + Current Cycle + {paymentDetails.currentCycle} + + + Contribution + ₹{paymentDetails.contributionAmount} + + + Total Due + ₹{paymentDetails.totalDueWithLateFee} + + + + Participants + {paymentDetails.participants.map(renderParticipant)} + + + ); +}; + +const styles = StyleSheet.create({ + centered: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + container: { + flex: 1, + backgroundColor: '#f4f6f8', + }, + header: { + + padding: 20, + flexDirection: 'row', + alignItems: 'center', + borderBottomLeftRadius: 20, + borderBottomRightRadius: 20, + }, + backButton: { + marginRight: 15, + }, + headerTitle: { + color: '#000000ff', + fontSize: 22, + fontWeight: 'bold', + }, + summaryContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + padding: 20, + backgroundColor: '#fff', + margin: 15, + borderRadius: 10, + }, + summaryBox: { + alignItems: 'center', + }, + summaryLabel: { + fontSize: 14, + color: '#666', + }, + summaryValue: { + fontSize: 18, + fontWeight: 'bold', + color: colors.primary, + }, + sectionTitle: { + fontSize: 18, + fontWeight: 'bold', + marginLeft: 20, + marginBottom: 10, + }, + participantCard: { + backgroundColor: '#fff', + marginHorizontal: 15, + marginBottom: 10, + borderRadius: 10, + padding: 15, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + participantInfo: { + flex: 1, + }, + participantName: { + fontSize: 16, + fontWeight: 'bold', + }, + participantMobile: { + fontSize: 14, + color: '#888', + }, + paymentStatus: { + alignItems: 'flex-end', + }, + statusText: { + fontSize: 14, + fontWeight: 'bold', + }, + amountText: { + fontSize: 14, + }, + payButton: { + marginTop: 5, + backgroundColor: colors.primary, + paddingVertical: 5, + paddingHorizontal: 10, + borderRadius: 5, + }, + payButtonText: { + color: '#fff', + fontSize: 12, + }, + retryButton: { + marginTop: 10, + padding: 10, + }, + retryButtonText: { + color: colors.primary, + fontSize: 16, + }, +}); + +export default Payments; + diff --git a/src/screens/profile/BecomeOrg.tsx b/src/screens/profile/BecomeOrg.tsx index 0d63471..c0ed110 100644 --- a/src/screens/profile/BecomeOrg.tsx +++ b/src/screens/profile/BecomeOrg.tsx @@ -8,7 +8,6 @@ import { ScrollView, KeyboardAvoidingView, Platform, - SafeAreaView, TouchableOpacity, } from "react-native"; import * as Keychain from "react-native-keychain"; @@ -16,17 +15,17 @@ import { CustomInput, CustomButton } from "../../components/common/Custom"; import colors from "../../components/colors"; import { useNavigation } from "@react-navigation/native"; import AppView from "../../components/AppView"; -import Ionicons from "@react-native-vector-icons/ionicons"; +import { Ionicons } from "@react-native-vector-icons/ionicons"; const BecomeOrg = () => { const navigation = useNavigation(); - const [bankAccountNumber, setBankAccountNumber] = useState(""); - const [bankName, setBankName] = useState(""); - const [branchCode, setBranchCode] = useState(""); + const [idName, setIdName] = useState(""); + const [idNumber, setIdNumber] = useState(""); + const [issueDate, setIssueDate] = useState(""); const [loading, setLoading] = useState(false); const handleSubmit = async () => { - if (!bankAccountNumber || !bankName || !branchCode) { + if (!idName || !idNumber || !issueDate) { Alert.alert("Error", "Please fill all fields"); return; } @@ -35,7 +34,10 @@ const BecomeOrg = () => { setLoading(true); const credentials = await Keychain.getGenericPassword(); if (!credentials) { - Alert.alert("Error", "You are not logged in"); + Alert.alert("Error", "You are not logged in. Please log in to continue."); + setLoading(false); + // Optionally navigate to login screen + // navigation.navigate("Login"); return; } const token = credentials.password; @@ -45,31 +47,31 @@ const BecomeOrg = () => { { method: "POST", headers: { - Accept: "*/*", "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ - bankAccountNumber, - bankName, - branchCode, + idName, + idNumber, + issueDate, }), } ); const result = await response.json(); - if (result.status === "success") { - Alert.alert("Success", "Organizer registration applied successfully"); - setBankAccountNumber(""); - setBankName(""); - setBranchCode(""); + + if (response.ok && result.status === "success") { + Alert.alert("Success", "Organizer registration application submitted successfully!"); + setIdName(""); + setIdNumber(""); + setIssueDate(""); navigation.navigate("Home" as never); } else { - Alert.alert("Error", result.message || "Failed to apply"); + Alert.alert("Error", result.message || "Failed to submit application"); } } catch (error) { console.error("❌ Registration error:", error); - Alert.alert("Error", "Something went wrong"); + Alert.alert("Error", "An unexpected error occurred. Please try again."); } finally { setLoading(false); } @@ -78,11 +80,11 @@ const BecomeOrg = () => { return ( - navigation.replace("Home" as never)} > + navigation.goBack()} style={styles.backButton}> - Become Organizer - + Become an Organizer + { contentContainerStyle={styles.container} keyboardShouldPersistTaps="handled" > - Organizer Registration + Submit Your ID for Verification + + Please provide your official identification details to become a verified organizer. + + placeholder="ID Name (e.g., Aadhaar, PAN Card)" + value={idName} + onChangeText={setIdName} + icon="person-outline" + error={""} + /> {loading && ( @@ -148,23 +155,38 @@ const styles = StyleSheet.create({ justifyContent: "center", }, title: { - fontSize: 22, + fontSize: 24, fontWeight: "700", - marginBottom: 20, + marginBottom: 10, textAlign: "center", color: colors.textDark, }, + subtitle: { + fontSize: 16, + textAlign: "center", + color: colors.textDark, + marginBottom: 30, + }, header: { flexDirection: "row", - justifyContent: "space-between", alignItems: "center", - padding: 16, - borderBottomLeftRadius: 20, - borderBottomRightRadius: 20, + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: '#FFF', + borderBottomWidth: 1, + borderBottomColor: '#E0E0E0', + }, + backButton: { + padding: 5, }, headerTitle: { - fontSize: 22, - fontWeight: "700", - color: "#000" + flex: 1, + fontSize: 20, + fontWeight: "600", + color: "#000", + textAlign: "center", + }, + placeholder: { + width: 24, // to balance the back button }, }); 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/`;