homePage update

This commit is contained in:
Abin Toppo
2025-09-25 15:46:17 +05:30
18 changed files with 1079 additions and 508 deletions
+4 -2
View File
@@ -3,6 +3,7 @@
import React, { FC } from 'react'; import React, { FC } from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import colors from './colors'; import colors from './colors';
import { useNavigation } from '@react-navigation/native';
// Update the type to match the full data structure // Update the type to match the full data structure
interface JoinedGroup { interface JoinedGroup {
@@ -63,9 +64,10 @@ const getStatusStyle = (status: JoinedGroup['status']) => {
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress }) => { export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress }) => {
const progress = (item.currentParticipants / item.maxParticipants) * 100; const progress = (item.currentParticipants / item.maxParticipants) * 100;
const statusStyle = getStatusStyle(item.status); const statusStyle = getStatusStyle(item.status);
const navigation = useNavigation<any>()
return ( return (
<View style={styles.card}> <TouchableOpacity style={styles.card} onPress={()=> navigation.navigate("GroupDetails", {item, color:colors.Settings})}>
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.groupName}>{item.groupName}</Text> <Text style={styles.groupName}>{item.groupName}</Text>
@@ -107,7 +109,7 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress })
<Text style={styles.viewButtonText}>View</Text> <Text style={styles.viewButtonText}>View</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View> </TouchableOpacity>
); );
}; };
+2 -1
View File
@@ -10,12 +10,13 @@ const colors = {
Profile: "#2D9CDB", // Deep sky blue Profile: "#2D9CDB", // Deep sky blue
Group: "#27AE60", // Green for freshness Group: "#27AE60", // Green for freshness
Settings: "#F39C12", // Amber for warmth Settings: "#F39C12", // Amber for warmth
// common text + UI colors // common text + UI colors
textDark: "#212121", textDark: "#212121",
textLight: "#555555", textLight: "#555555",
white: "#FFFFFF", white: "#FFFFFF",
black: "#000000", black: "#000000",
primary: "#a57ae9ff", primary: "#711ff7ff",
}; };
export default colors; export default colors;
+158
View File
@@ -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<CustomModalProps> = ({
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 (
<Modal transparent visible={visible} animationType="none">
{/* Overlay */}
<Animated.View style={[styles.overlay, { opacity: fadeAnim }]}>
{/* Modal Box */}
<Animated.View
style={[
styles.modalBox,
{ transform: [{ translateY: slideAnim }] },
]}
>
{/* Header */}
{
title &&
<Text style={styles.title}>{title}</Text>
}
{/* Body */}
<View style={styles.content}>{children}</View>
{/* Footer Actions */}
<View style={styles.actions}>
<TouchableOpacity
style={[styles.button, styles.cancelButton]}
onPress={onClose}
>
<Text style={styles.cancelText}>{cancelText}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, {backgroundColor:confirmButton}]}
onPress={onConfirm}
>
<Text style={styles.confirmText}>{confirmText}</Text>
</TouchableOpacity>
</View>
</Animated.View>
</Animated.View>
</Modal>
);
};
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",
},
});
+1
View File
@@ -57,5 +57,6 @@ const styles = StyleSheet.create({
headerTitle: { headerTitle: {
fontSize: 18, fontSize: 18,
fontWeight: "600", fontWeight: "600",
textTransform:'uppercase'
}, },
}); });
+1 -1
View File
@@ -30,7 +30,7 @@ export type RootStackParamList = {
GroupCreation: undefined; GroupCreation: undefined;
GroupReview: undefined; GroupReview: undefined;
EditGroup: { groupId: string | number }; EditGroup: { groupId: string | number };
GroupDetails: { groupId: string | number }; GroupDetails: { item:object };
BecomeOrg: undefined; BecomeOrg: undefined;
YourContacts: undefined; YourContacts: undefined;
Transaction: undefined; Transaction: undefined;
+17 -19
View File
@@ -1,5 +1,5 @@
import React from "react"; 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 { View, Text, StyleSheet, Platform } from "react-native";
import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons"; import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons";
import GroupScreen from "../screens/GroupScreen"; import GroupScreen from "../screens/GroupScreen";
@@ -9,7 +9,6 @@ import colors from "../components/colors";
import Setting from "../screens/setting/Setting"; import Setting from "../screens/setting/Setting";
import ManagementScreen from "../screens/management/ManagementScreen"; import ManagementScreen from "../screens/management/ManagementScreen";
const Tab = createBottomTabNavigator();
const TAB_CONFIG = { const TAB_CONFIG = {
activeColor: "#000", activeColor: "#000",
@@ -29,26 +28,24 @@ const activeTabColors: { [key: string]: string } = {
// Icon/Text colors on active tabs // Icon/Text colors on active tabs
const activeIconColors: { [key: string]: string } = { const activeIconColors: { [key: string]: string } = {
Home: "#333", Home: "#333",
Profile: "#333", Profile: "#333",
Group: "#333", Group: "#333",
Management: "#333", Management: "#333",
} }
function HomeScreen() { export type RootTabParamList = {
return ( Home: undefined;
<View style={styles.screen}><Text style={styles.text}>🏠 Home</Text></View> 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<RootTabParamList>;
const Tab = createBottomTabNavigator<RootTabParamList>();
function SettingsScreen() { const BottomTabs: React.FC = () => {
return (
<View style={styles.screen}><Text style={styles.text}>⚙️ Settings</Text></View>
);
}
export default function BottomTabs() {
return ( return (
<Tab.Navigator <Tab.Navigator
screenOptions={({ route }) => ({ screenOptions={({ route }) => ({
@@ -62,7 +59,7 @@ export default function BottomTabs() {
tabBarIcon: ({ focused }) => { tabBarIcon: ({ focused }) => {
let iconName: IoniconsIconName = "home-outline"; let iconName: IoniconsIconName = "home-outline";
// Determine icon based on route // Determine icon based on route
if (route.name === "Home") iconName = focused ? "home" : "home-outline"; if (route.name === "Home") iconName = focused ? "home" : "home-outline";
else if (route.name === "Profile") iconName = focused ? "person" : "person-outline"; else if (route.name === "Profile") iconName = focused ? "person" : "person-outline";
@@ -151,3 +148,4 @@ const styles = StyleSheet.create({
shadowRadius: 5, shadowRadius: 5,
}, },
}); });
export default BottomTabs
+42
View File
@@ -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<T extends keyof RootStackParamList> = StackScreenProps<
RootStackParamList,
T
>;
+156
View File
@@ -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<ContributionProps> = ({
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 (
<CustomModal visible={visible} onClose={onClose} onConfirm={onConfirm}>
{/* Header */}
<Animated.View
style={[
styles.header,
{ opacity: fadeAnim, transform: [{ translateY: slideAnim }] },
]}
>
<Ionicons name="wallet-outline" size={32} color={colors.primary} />
<Text style={styles.headerTitle}>Contribution Summary</Text>
</Animated.View>
{/* Contribution Info Card */}
<Animated.View
style={[
styles.card,
{ transform: [{ scale: scaleAnim }], opacity: fadeAnim },
]}
>
<Text style={styles.label}>Base Amount</Text>
<Text style={styles.value}>₹{data?.baseAmount}</Text>
<Text style={styles.label}>Total Payable</Text>
<Text style={styles.valueHighlight}>₹{data?.totalPayable}</Text>
<Text style={styles.label}>Due Date</Text>
<Text style={styles.value}>{data?.dueDate}</Text>
<Text style={styles.label}>Cycle End</Text>
<Text style={styles.value}>{data?.cycleEnd}</Text>
</Animated.View>
{/* Extra Details */}
<Animated.View
style={[
styles.card,
{ transform: [{ translateY: slideAnim }], opacity: fadeAnim },
]}
>
<Text style={styles.label}>Cycle Number</Text>
<Text style={styles.value}>{data?.cycleNumber}</Text>
<Text style={styles.label}>Late Fee Mode</Text>
<Text style={styles.value}>{data?.lateFeeMode}</Text>
<Text style={styles.label}>Late Fee Value</Text>
<Text style={styles.value}>₹{data?.lateFeeValue}</Text>
<Text style={styles.label}>Effective Late Fee</Text>
<Text style={styles.value}>₹{data?.effectiveLateFee}</Text>
</Animated.View>
</CustomModal>
);
};
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,
},
});
+4 -3
View File
@@ -76,7 +76,7 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
try { try {
// Replace with your actual API call to join the group // Replace with your actual API call to join the group
// For now, it just shows an alert. // 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}".`); Alert.alert('Success!', `You have requested to join "${groupDetails.groupName}".`);
} catch (e: any) { } catch (e: any) {
Alert.alert('Join Error', e.message || 'An unexpected error occurred.'); Alert.alert('Join Error', e.message || 'An unexpected error occurred.');
@@ -99,11 +99,12 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
{/* Group Info Card */} {/* Group Info Card */}
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.cardTitle}>Group Details</Text> <Text style={styles.cardTitle}>Group Details </Text>
<InfoRow label="Organizer" value={organizerName} /> <InfoRow label="Organizer" value={organizerName} />
<InfoRow label="Contribution" value={`₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`} /> <InfoRow label="Contribution" value={`₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`} />
<InfoRow label="Payment Cycle" value={groupDetails.cycleType} /> <InfoRow label="Payment Cycle" value={groupDetails.cycleType} />
<InfoRow label="Status" value={groupDetails.status} /> <InfoRow label="Status" value={groupDetails.status} />
<InfoRow label="Code" value={code} />
</View> </View>
{/* Participants Card */} {/* Participants Card */}
@@ -143,7 +144,7 @@ const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
}; };
// Helper component for displaying rows // Helper component for displaying rows
const InfoRow = ({ label, value }: { label: string; value: string }) => ( const InfoRow = ({ label, value }: { label: string; value: string | number }) => (
<View style={styles.infoRow}> <View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}</Text> <Text style={styles.infoLabel}>{label}</Text>
<Text style={styles.infoValue}>{value}</Text> <Text style={styles.infoValue}>{value}</Text>
+120 -83
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
StyleSheet, StyleSheet,
Text, Text,
@@ -8,86 +8,70 @@ import {
Platform, Platform,
Image, Image,
Alert, Alert,
FlatList,
ListRenderItem, // Import ListRenderItem for better typing
} from "react-native"; } from "react-native";
import AppView from "../../components/AppView"; import AppView from "../../components/AppView";
import UserProfileStatus from "./UserProfileStatus";
import colors from "../../components/colors"; import colors from "../../components/colors";
import GroupsTabs from "./GroupsTabs";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RouteProp, useFocusEffect, useNavigation, useRoute } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import UserHome from "./user-home/UserHome";
import { PROFILE_URL } from "../../utils/ApiUrls"; 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"; 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) { if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true); UIManager.setLayoutAnimationEnabledExperimental(true);
} }
type RootStackParamList = { // --- TYPE DEFINITIONS ---
Home: { refreshProfile: boolean; role: string }; interface ActionItem {
};
type UserProfile = {
id: string; id: string;
userId: number; title: string;
firstName: string | null; action: string;
lastName: string | null; tab?: "My Groups" | "Joined Groups" | "Requests" | "Your Requests";
imageBlob: string | null; imageUri: string;
role?: string;
} }
const Home = () => {
const user = useSelector((state: any) => state.user);
const navigation = useNavigation();
const dispatch = useDispatch()
const [apiUser, setApiUser] = useState<UserProfile | null | any>(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<TabNavigationProp>();
const dispatch = useDispatch();
// --- FIXED: Conditional logic moved inside the component ---
const actions = user.role === 'ORGANIZER' ? organizerActions : userActions;
const fetchUserProfile = async () => { const fetchUserProfile = async () => {
try { try {
const credentials = await Keychain.getGenericPassword(); const credentials = await Keychain.getGenericPassword();
if (!credentials) { if (!credentials) return;
return;
}
const token = credentials.password; const token = credentials.password;
const response = await fetch(
PROFILE_URL, const response = await fetch(PROFILE_URL, {
{ method: "GET",
method: "GET", headers: { Accept: "*/*", Authorization: `Bearer ${token}` },
headers: { });
Accept: "*/*",
Authorization: `Bearer ${token}`,
},
}
);
const result = await response.json(); const result = await response.json();
console.log("👤 Profile Response:", result);
if (result.status === "success") { if (result.status === "success") {
setApiUser(result.data); // No need for apiUser state, just dispatch to Redux
dispatch( dispatch(setUserData({ ...result.data, profileStatus: true }));
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,
})
);
} }
} catch (error) { } catch (error) {
console.error("❌ Error fetching profile:", error); console.error("❌ Error fetching profile:", error);
@@ -98,27 +82,45 @@ const Home = () => {
fetchUserProfile(); 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<ActionItem> = ({ item }) => (
<TouchableOpacity
style={styles.gridButton}
onPress={() => handleActionPress(item)}
>
{/* FIXED: 'imageUri' was missing from your data objects */}
<Image source={{ uri: item.imageUri }} style={styles.gridIcon} />
<Text style={styles.gridLabel}>{item.title}</Text>
</TouchableOpacity>
);
return ( return (
<AppView barStyle="dark-content" style={styles.container}> <AppView barStyle="dark-content" style={styles.container}>
{/* Header */}
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.greeting}>Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}👋</Text> <Text style={styles.headerTitle}>Home</Text>
<TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}>
<Image
source={
user.imageBlob
&& { uri: user.imageBlob || apiUser?.imageBlob ? `data:image/jpeg;base64,${user.imageBlob || apiUser?.imageBlob}` : user.profilePicture || apiUser?.imageBlob }
}
style={{ width: 40, height: 40, borderRadius: 20, borderColor: colors.primary, borderWidth: 2 }}
/>
</TouchableOpacity>
</View> </View>
{/* Status */}
<UserProfileStatus /> <FlatList
{/* <UserHome /> */} data={actions}
{user?.role === "USER" || apiUser?.role === "USER" ? <UserHome /> : renderItem={renderGridItem}
<GroupsTabs />} keyExtractor={(item) => item.id}
numColumns={2}
columnWrapperStyle={styles.row}
contentContainerStyle={{ paddingHorizontal: 8 }}
/>
</AppView> </AppView>
); );
}; };
@@ -127,13 +129,48 @@ export default Home;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
paddingBottom: '25%', flex: 1,
backgroundColor: '#F4F6F8'
}, },
header: { header: {
flexDirection: "row", justifyContent: "center",
justifyContent: "space-between",
alignItems: "center", alignItems: "center",
marginBottom: 12, paddingVertical: 12,
marginBottom: 16,
}, },
greeting: { fontSize: 22, fontWeight: "600", color: colors.Home }, 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",
},
});
+162 -149
View File
@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from "react"; import React, { useRef, useEffect, useState } from "react";
import { import {
StyleSheet, StyleSheet,
Text, Text,
@@ -6,108 +6,161 @@ import {
TouchableOpacity, TouchableOpacity,
FlatList, FlatList,
Animated, Animated,
ToastAndroid,
Alert,
} from "react-native"; } from "react-native";
import { useRoute } from "@react-navigation/native"; import { useRoute, RouteProp } from "@react-navigation/native";
import AppView from "../../../components/AppView"; import AppView from "../../../components/AppView";
import colors from "../../../components/colors"; 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 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 GroupDetails = () => {
const route = useRoute(); const route = useRoute<any>();
const { groupId } = route.params as { groupId: string }; const { id, groupName, description, contributionAmount, currentParticipants } = route.params;
const [memberData, setMemberData] = useState<Member[]>([]);
const [loading, setLoading] = useState(false);
const [openModal, setOpenModal] = useState(false)
const [payable, setPayable] = useState()
// Group object
const group = { const group = {
id: groupId, id: route.params.item?.id,
name: "Wedding Fund", name: route.params.item?.groupName,
collected: 12000, collected: route.params.item?.contributionAmount,
goal: 20000, goal: route.params.item?.contributionAmount * route.params.item?.currentParticipants,
description: description: route.params.item?.description,
"This group is created to collect contributions for the wedding celebration. Everyone can contribute as per their convenience.", status: route.params.item?.status,
members: [ members: memberData,
{ 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" },
],
}; };
const progress = Math.min(group.collected / group.goal, 1); const progress = Math.min(group.collected / group.goal, 1);
// Animation refs // Animation refs
const animatedValues = useRef(group.members.map(() => new Animated.Value(0))).current; const animatedValues = useRef<Animated.Value[]>([]);
// useEffect(() => {
// animatedValues.current = group.members.map(() => new Animated.Value(0));
// }, [group.members]);
// useEffect(() => {
// const animations = group.members.map((_, i) =>
// Animated.timing(animatedValues.current[i], {
// toValue: 1,
// duration: 400,
// delay: i * 100,
// useNativeDriver: true,
// })
// );
// Animated.stagger(100, animations).start();
// }, [group.members]);
// Fetch group members
useEffect(() => { useEffect(() => {
const animations = group.members.map((_, i) => const handleGroupDetails = async () => {
Animated.timing(animatedValues[i], { try {
toValue: 1, setLoading(true);
duration: 400, const res = await apiCall(`${GROUP_ALL_MEMBER}?groupId=${route?.params?.item?.id}`, "GET") as any;
delay: i * 100, console.log("Group details response:", res);
useNativeDriver: true, setMemberData(res.data);
}) ToastAndroid.show("Group details fetched successfully!", ToastAndroid.SHORT);
); } catch (error: any) {
Animated.stagger(100, animations).start(); 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 }) => { handleGroupDetails();
return ( }, [id]);
<Animated.View
style={[
styles.memberCard,
{
opacity: animatedValues[index],
transform: [
{
translateY: animatedValues[index].interpolate({
inputRange: [0, 1],
outputRange: [20, 0], // slide up effect
}),
},
],
},
]}
>
<View style={{ flexDirection: 'row', gap: 10 }}>
<Ionicons
name="person-circle-outline"
size={40}
color={colors.primary}
/>
<View>
<Text style={styles.memberName}>
{item.name} {item.role === "Organizer" ? "(Organizer)" : "(Member)"}
</Text>
<Text style={styles.memberDetail}>Contribution: ₹{item.contribution}</Text>
</View>
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 }) => (
<View
style={[
styles.memberCard,
{
// opacity: animatedValues.current[index],
// transform: [
// {
// translateY: animatedValues.current[index].interpolate({
// inputRange: [0, 1],
// outputRange: [20, 0],
// }),
// },
// ],
borderColor: route.params?.color ? route.params?.color : colors.Home
},
]}
>
<View style={{ flexDirection: "row", gap: 10 }}>
<Ionicons name="person-circle-outline" size={40} color={route.params?.color ? route.params?.color : colors.primary} />
<View>
<Text style={styles.memberName}>
{item.firstName} {item.lastName} {item.role === "Organizer" ? "(Organizer)" : "(Member)"}
</Text>
<Text style={styles.memberDetail}>Mobile: {item.mobile}</Text>
</View> </View>
</View>
</Animated.View> </View>
); );
};
return ( return (
<AppView> <AppView>
{/* Group Title */} <Header title={group.name} arrowColor={route.params?.color ? route.params?.color : colors.Home} titleColor={route.params?.color ? route.params?.color : colors.Home} />
<Header title={group.name}/>
{/* <Text style={styles.groupName}>{group.name}</Text> */}
<Text style={styles.description}>{group.description}</Text> <Text style={styles.description}>{group.description}</Text>
{/* Progress Bar */} {/* Progress Bar */}
<View style={styles.progressContainer}> <View style={styles.progressContainer}>
<View style={styles.progressBar}> <View style={styles.progressBar}>
<View style={[styles.progressFill, { flex: progress }]} /> <View style={{ flex: progress, backgroundColor: route.params?.color ? route.params?.color : colors.Home }} />
<View style={{ flex: 1 - progress }} /> <View style={{ flex: 1 - progress }} />
</View> </View>
<Text style={styles.amount}> <Text style={styles.amount}>₹{group.collected} / ₹{group.goal}</Text>
₹{group.collected} / ₹{group.goal}
</Text>
</View> </View>
{/* Stats */} {/* Stats */}
<View style={styles.statsRow}> <View style={styles.statsRow}>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statValue}>{group.members.length}</Text> <Text style={styles.statValue}>{group.members.length ?? []}</Text>
<Text style={styles.statLabel}>Members</Text> <Text style={styles.statLabel}>Members</Text>
</View> </View>
<View style={styles.statBox}> <View style={styles.statBox}>
@@ -121,42 +174,53 @@ const GroupDetails = () => {
</View> </View>
{/* Members List */} {/* Members List */}
<Text style={styles.sectionTitle}>Members</Text> <View style={styles.statsRow}>
<Text style={styles.sectionTitle}>Members</Text>
<Text style={[styles.sectionTitle, { backgroundColor: route.params?.color ? colors.pastelOrange : colors.pastelPink, color: route.params?.color ? colors.Settings : colors.Home, paddingHorizontal: 5 }]}>{group.status}</Text>
</View>
<FlatList <FlatList
data={group.members} data={group.members}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
renderItem={({ item, index }) => renderMember({ item, index })} renderItem={renderMember}
contentContainerStyle={styles.membersListContainer} contentContainerStyle={styles.membersListContainer}
/> />
{/* Actions */} {/* Actions */}
<View style={styles.actions}> {
<TouchableOpacity style={styles.button}> group.status === 'ACTIVE' &&
<Text style={styles.buttonText}>Contribute</Text>
</TouchableOpacity> <View style={styles.actions}>
</View> <TouchableOpacity onPress={() => {
setOpenModal(true)
handleContribution()
}}
style={[styles.button, { backgroundColor: route.params?.color ? route.params?.color : colors.Home }]}>
<Text style={styles.buttonText}>Contribute</Text>
</TouchableOpacity>
</View>
}
{
openModal && <ContributionScreen data={payable} visible={openModal} onClose={() => setOpenModal(!openModal)} onConfirm={() => {
confirmPayment()
setOpenModal(false)
}} />
}
</AppView> </AppView>
); );
}; };
export default GroupDetails; export default GroupDetails;
// Styles remain mostly unchanged
const styles = StyleSheet.create({ const styles = StyleSheet.create({
groupName: {
fontSize: 22,
fontWeight: "700",
color: "#0f172a",
marginBottom: 6,
},
description: { description: {
fontSize: 14, fontSize: 14,
color: "#475569", color: "#475569",
marginBottom: 20, marginBottom: 20,
lineHeight: 20, lineHeight: 20,
}, },
progressContainer: { progressContainer: { marginBottom: 24 },
marginBottom: 24,
},
progressBar: { progressBar: {
flexDirection: "row", flexDirection: "row",
height: 8, height: 8,
@@ -165,18 +229,8 @@ const styles = StyleSheet.create({
overflow: "hidden", overflow: "hidden",
marginBottom: 6, marginBottom: 6,
}, },
progressFill: { amount: { fontSize: 13, color: "#64748b" },
backgroundColor: colors.Home, statsRow: { flexDirection: "row", justifyContent: "space-between", marginBottom: 20 },
},
amount: {
fontSize: 13,
color: "#64748b",
},
statsRow: {
flexDirection: "row",
justifyContent: "space-between",
marginBottom: 20,
},
statBox: { statBox: {
flex: 1, flex: 1,
backgroundColor: "#fff", backgroundColor: "#fff",
@@ -190,61 +244,20 @@ const styles = StyleSheet.create({
shadowRadius: 6, shadowRadius: 6,
elevation: 2, elevation: 2,
}, },
statValue: { statValue: { fontSize: 18, fontWeight: "700", color: "#1e293b" },
fontSize: 18, statLabel: { fontSize: 12, color: "#64748b", marginTop: 4 },
fontWeight: "700", sectionTitle: { fontSize: 16, fontWeight: "600", color: "#0f172a", marginBottom: 12 },
color: "#1e293b", membersListContainer: { paddingBottom: 20 },
},
statLabel: {
fontSize: 12,
color: "#64748b",
marginTop: 4,
},
sectionTitle: {
fontSize: 16,
fontWeight: "600",
color: "#0f172a",
marginBottom: 12,
},
membersListContainer: {
paddingBottom: 20,
},
memberCard: { memberCard: {
backgroundColor: "#fff", backgroundColor: "#fff",
padding: 16, padding: 16,
borderRadius: 12, borderRadius: 12,
marginBottom: 10, marginBottom: 10,
// shadowColor: "#000", borderLeftWidth: 5,
// 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",
}, },
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" },
}); });
+24 -8
View File
@@ -6,6 +6,7 @@ import {
FlatList, FlatList,
TouchableOpacity, TouchableOpacity,
Alert, Alert,
RefreshControl,
} from "react-native"; } from "react-native";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
@@ -34,13 +35,25 @@ const UserHome = () => {
const navigation = useNavigation<NavigationProp>(); const navigation = useNavigation<NavigationProp>();
const [groups, setGroups] = useState<Group[]>([]); const [groups, setGroups] = useState<Group[]>([]);
const [loading, setLoading] = useState<boolean>(false) const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(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, (sum, g) => sum + g.contributionAmount,
0 0
); );
const onRefresh = async () => {
setRefreshing(true)
await handleGroup()
setRefreshing(false)
}
const handleGroup = async () => { const handleGroup = async () => {
try { try {
setLoading(true) setLoading(true)
@@ -70,7 +83,7 @@ const UserHome = () => {
} catch (error) { } catch (error) {
console.error("❌ Error fetching groups:", error); console.error("❌ Error fetching groups:", error);
Alert.alert("Error", "Could not connect to server"); Alert.alert("Error", "Could not connect to server");
}finally{ } finally {
setLoading(false) setLoading(false)
} }
}; };
@@ -83,11 +96,10 @@ const UserHome = () => {
return ( return (
<TouchableOpacity <TouchableOpacity
style={styles.card} style={styles.card}
onPress={() => navigation.navigate("GroupDetails", { groupId: item.id })} onPress={() => navigation.navigate("GroupDetails", { item })}
> >
<Text style={styles.groupName}>{item.groupName}</Text> <Text style={styles.groupName}>{item.groupName}</Text>
<Text style={styles.description}>{item.description}</Text> <Text style={styles.description} numberOfLines={1}>{item.description}</Text>
<View style={styles.statsRow}> <View style={styles.statsRow}>
<Ionicons <Ionicons
name="person-circle-outline" name="person-circle-outline"
@@ -117,17 +129,19 @@ const UserHome = () => {
</View> </View>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statValue}>₹{totalContribution}</Text> <Text style={styles.statValue}>₹{totalContribution}</Text>
<Text style={styles.statLabel}>Total Contribution</Text> <Text style={styles.statLabel}>Cycle Contribution</Text>
</View> </View>
</View> </View>
{/* Groups List */} {/* Groups List */}
<Text style={styles.sectionTitle}>Your Groups</Text> <Text style={styles.sectionTitle}>Your Groups</Text>
<FlatList <FlatList
data={groups} data={groups.filter((status) => status.status === "ACTIVE")}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
renderItem={renderItem} renderItem={renderItem}
contentContainerStyle={{ paddingBottom: 20 }} contentContainerStyle={{ paddingBottom: 20 }}
onRefresh={onRefresh}
refreshing={refreshing}
/> />
</View> </View>
); );
@@ -178,6 +192,8 @@ const styles = StyleSheet.create({
shadowOffset: { width: 0, height: 2 }, shadowOffset: { width: 0, height: 2 },
shadowRadius: 6, shadowRadius: 6,
elevation: 2, elevation: 2,
borderLeftWidth: 5,
borderColor: colors.Home
}, },
groupName: { groupName: {
fontSize: 16, fontSize: 16,
+2 -2
View File
@@ -338,6 +338,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
{renderCard('Financial Rules', [ {renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` }, { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` }, { 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')}` }, { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
], 'financialRules')} ], 'financialRules')}
@@ -403,7 +404,6 @@ const styles = StyleSheet.create({
paddingHorizontal: 16, paddingHorizontal: 16,
paddingTop: 16, paddingTop: 16,
paddingBottom: 8, paddingBottom: 8,
backgroundColor: 'white', // Changed for a cleaner header look
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: '#E5E7EB', borderBottomColor: '#E5E7EB',
}, },
@@ -436,7 +436,7 @@ const styles = StyleSheet.create({
saveButton: { backgroundColor: colors.pastelOrange }, saveButton: { backgroundColor: colors.pastelOrange },
cancelButton: { backgroundColor: '#E5E7EB' }, cancelButton: { backgroundColor: '#E5E7EB' },
inviteButton: { backgroundColor: colors.primary }, inviteButton: { backgroundColor: colors.primary },
buttonText: { color: '#000000ff', fontSize: 15, fontWeight: '600' }, buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' },
// --- FLOATING BUTTON STYLES --- // --- FLOATING BUTTON STYLES ---
floatingButtonContainer: { floatingButtonContainer: {
+71 -183
View File
@@ -1,6 +1,6 @@
// src/screens/ManagementScreen.tsx // src/screens/ManagementScreen.tsx
import React, { useState, useCallback, FC } from "react"; import React, { useState, useCallback, FC, useEffect } from "react";
import { import {
View, View,
Text, Text,
@@ -9,69 +9,74 @@ import {
TouchableOpacity, TouchableOpacity,
ActivityIndicator, ActivityIndicator,
Alert, Alert,
ListRenderItemInfo,
} from "react-native"; } 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 { StackNavigationProp } from "@react-navigation/stack";
import { RootStackParamList } from "../../navigation/AppNavigator"; import { APPROVE_REQUEST_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, MY_GROUPS_URL, REQUESTS_URL, USER_REQUEST } from "../../utils/ApiUrls";
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 colors from "../../components/colors"; import colors from "../../components/colors";
import AppView from "../../components/AppView"; import AppView from "../../components/AppView";
import { apiCall } from "../../utils/ApiHelper"; import { apiCall } from "../../utils/ApiHelper";
import * as Keychain from "react-native-keychain"; import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types'; // Adjust import path
import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types';
import { MyGroupCard } from "../../components/MyGroupCard"; import { MyGroupCard } from "../../components/MyGroupCard";
import { JoinedGroupCard } from "../../components/JoinedGroupCard"; import { JoinedGroupCard } from "../../components/JoinedGroupCard";
import { RequestCard } from "../../components/RequestCard"; import { RequestCard } from "../../components/RequestCard";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import Ionicons from "@react-native-vector-icons/ionicons";
import { UserRequested } from "../../components/UserRequested"; import { UserRequested } from "../../components/UserRequested";
import { RootTabParamList } from "../../navigation/BottomTabs"; // Adjust import path
type GroupManagementNavProp = StackNavigationProp<RootStackParamList>; // --- TYPE DEFINITIONS ---
type GroupManagementNavProp = StackNavigationProp<RootTabParamList, 'Management'>;
type ManagementRouteProp = RouteProp<RootTabParamList, 'Management'>;
type ActiveTab = "My Groups" | "Joined Groups" | "Requests" | "Your Requests"; type ActiveTab = "My Groups" | "Joined Groups" | "Requests" | "Your Requests";
const ManagementScreen: FC = () => { const ManagementScreen: FC = () => {
const userRole = useSelector((state: any) => state.user) const user = useSelector((state: any) => state.user);
const [activeTab, setActiveTab] = useState<ActiveTab>(userRole.role === "USER" ? "Joined Groups" : "My Groups");
const navigation = useNavigation<GroupManagementNavProp>(); const navigation = useNavigation<GroupManagementNavProp>();
const route = useRoute<ManagementRouteProp>();
const [activeTab, setActiveTab] = useState<ActiveTab>(user.role === "USER" ? "Joined Groups" : "My Groups");
const [myGroups, setMyGroups] = useState<MyGroup[]>([]); const [myGroups, setMyGroups] = useState<MyGroup[]>([]);
const [joinedGroups, setJoinedGroups] = useState<JoinedGroup[]>([]); const [joinedGroups, setJoinedGroups] = useState<JoinedGroup[]>([]);
const [requests, setRequests] = useState<JoinRequest[]>([]); const [requests, setRequests] = useState<JoinRequest[]>([]);
const [userRequest, setUserRequest] = useState<UserJoinRequest[]>([]);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [userRequest, setUserRequest] = useState<UserJoinRequest[]>([])
// --- 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 () => { const fetchData = useCallback(async () => {
// Check for loading state at the start to prevent multiple simultaneous fetches
if (loading) return; if (loading) return;
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
if (userRole.role === "ORGANIZER") { if (user.role === "ORGANIZER") {
// Fetch all data concurrently for organizers
const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([ const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([
apiCall(MY_GROUPS_URL, "GET") as Promise<ApiResponse<MyGroup[]>>, apiCall(MY_GROUPS_URL, "GET") as Promise<ApiResponse<MyGroup[]>>,
apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>, apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>,
apiCall(REQUESTS_URL, "GET") as Promise<ApiResponse<JoinRequest[]>>, apiCall(REQUESTS_URL, "GET") as Promise<ApiResponse<JoinRequest[]>>,
]); ]);
console.log("My Groups:", myGroupsRes.data);
console.log("Joined Groups:", joinedGroupsRes.data);
console.log("Requests:", requestsRes.data);
setMyGroups(myGroupsRes.data || []); setMyGroups(myGroupsRes.data || []);
setJoinedGroups(joinedGroupsRes.data || []); setJoinedGroups(joinedGroupsRes.data || []);
setRequests(requestsRes.data || []); setRequests(requestsRes.data || []);
} else { } else {
// Fetch only joined groups and user requests for non-organizers
const [joinedGroupsRes, userRequestRes] = await Promise.all([ const [joinedGroupsRes, userRequestRes] = await Promise.all([
apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>, apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>,
apiCall(USER_REQUEST, "GET") as Promise<ApiResponse<UserJoinRequest[]>>, apiCall(USER_REQUEST, "GET") as Promise<ApiResponse<UserJoinRequest[]>>,
]); ]);
console.log("Joined Groups:", joinedGroupsRes.data);
console.log("User Requests:", userRequestRes.data);
setJoinedGroups(joinedGroupsRes.data || []); setJoinedGroups(joinedGroupsRes.data || []);
setUserRequest(userRequestRes.data || []); setUserRequest(userRequestRes.data || []);
} }
@@ -81,113 +86,64 @@ const ManagementScreen: FC = () => {
} finally { } finally {
setLoading(false); 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) => { const handleViewGroup = (item: JoinedGroup) => {
console.log("PPP",item) // Assuming you have an 'Invite' screen in your RootTabParamList or another navigator
navigation.navigate('Invite', { groupId: item.id } as any); // navigation.navigate('Invite' as any, { groupId: item.id });
} };
const handleAction = async (url: string, body: object, successMessage: string, failureMessage: string, action: string) => { const handleAction = async (url: string, successMessage: string, failureMessage: string) => {
// ... (logic remains the same)
console.log(url)
try { try {
setLoading(true); // Optionally show a loading state during the action // No need to set loading here if the whole screen is already loading
// Make the POST request with an empty body
const res = await apiCall(url, "PUT", {}) as ApiResponse<null> | null; const res = await apiCall(url, "PUT", {}) as ApiResponse<null> | null;
if (res && res.statusCode === 200) { if (res && res.statusCode === 200) {
// Use the success message from the API response Alert.alert("Success", res.message || successMessage);
Alert.alert("Success", res.message || `Request ${action}d successfully.`); fetchData(); // Refresh data after action
fetchData(); // Refresh the list of requests
} else { } else {
throw new Error(res?.message || failureMessage); throw new Error(res?.message || failureMessage);
} }
} catch (err: any) { } catch (err: any) {
Alert.alert("Error", err.message || "An unexpected error occurred."); 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 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=/" + " ", { requestId: req.requestId }, `Request from ${req.userName} has been declined.`, "Failed to decline request.", "Decline"); 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 }); const handleManageGroup = (group: MyGroup) => navigation.navigate('ManagementTabs', { groupId: group.id });
// --- RENDER LOGIC ---
const renderContent = () => { const renderContent = () => {
if (error) { if (error) return <Text style={styles.emptyText}>{error}</Text>;
return <Text style={styles.emptyText}>{error}</Text>; // Improved loading logic to avoid hiding existing data during a refresh
if (loading && myGroups.length === 0 && joinedGroups.length === 0 && requests.length === 0) {
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
} }
switch (activeTab) { switch (activeTab) {
case "My Groups": case "My Groups":
if (loading) { return <FlatList data={myGroups} renderItem={({ item }) => <MyGroupCard item={item} onManage={handleManageGroup} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't created any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
}
return (
<FlatList
data={myGroups}
renderItem={({ item }: ListRenderItemInfo<MyGroup>) => <MyGroupCard item={item} onManage={handleManageGroup} />}
keyExtractor={(item) => item.id}
ListEmptyComponent={<Text style={styles.emptyText}>You haven't created any groups yet.</Text>}
onRefresh={fetchData}
refreshing={loading} // This handles the "continuing loading" on pull-to-refresh
contentContainerStyle={{ paddingBottom: 100 }}
/>
);
case "Joined Groups": case "Joined Groups":
if (loading && joinedGroups.length === 0) { return <FlatList data={joinedGroups} renderItem={({ item }) => <JoinedGroupCard item={item} onViewPress={() => handleViewGroup(item)} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
}
return (
<FlatList
data={joinedGroups}
renderItem={({ item }: ListRenderItemInfo<JoinedGroup>) => <JoinedGroupCard item={item} onViewPress={() => handleViewGroup(item)} />}
keyExtractor={(item) => item.id}
ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>}
onRefresh={fetchData}
refreshing={loading} // This handles the "continuing loading" on pull-to-refresh
contentContainerStyle={{ paddingBottom: 100 }}
/>
);
case "Requests": case "Requests":
if (loading && requests.length === 0) { return <FlatList data={requests} renderItem={({ item }) => <RequestCard item={item} onApprove={handleApprove} onDecline={handleDecline} />} keyExtractor={(item) => item.requestId} ListEmptyComponent={<Text style={styles.emptyText}>No pending join requests.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
}
return (
<FlatList
data={requests}
renderItem={({ item }: ListRenderItemInfo<JoinRequest>) => <RequestCard item={item} onApprove={handleApprove} onDecline={handleDecline} />}
keyExtractor={(item) => item.requestId}
ListEmptyComponent={<Text style={styles.emptyText}>No pending join requests.</Text>}
onRefresh={fetchData}
refreshing={loading} // This handles the "continuing loading" on pull-to-refresh
contentContainerStyle={{ paddingBottom: 100 }}
/>
);
case "Your Requests": case "Your Requests":
if (loading && requests.length === 0) { return <FlatList data={userRequest} renderItem={({ item }) => <UserRequested item={item} />} keyExtractor={(item) => item.requestId} ListEmptyComponent={<Text style={styles.emptyText}>You haven't sent any join requests.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
}
return (
<FlatList
data={userRequest}
renderItem={({ item }: ListRenderItemInfo<UserJoinRequest>) => <UserRequested item={item} />}
keyExtractor={(item) => item.requestId}
ListEmptyComponent={<Text style={styles.emptyText}>No pending join requests.</Text>}
onRefresh={fetchData}
refreshing={loading} // This handles the "continuing loading" on pull-to-refresh
contentContainerStyle={{ paddingBottom: 100 }}
/>
);
default: return null; 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 ( return (
<AppView style={styles.container} paddingHorizontal={0}> <AppView style={styles.container} paddingHorizontal={0}>
<View style={styles.header}> <View style={styles.header}>
@@ -201,32 +157,6 @@ const ManagementScreen: FC = () => {
))} ))}
</View> </View>
<View style={styles.contentArea}> <View style={styles.contentArea}>
{
activeTab === "Requests" && userRole === "ORGANIZER" ? (
<TouchableOpacity
style={{
position: "absolute",
bottom: 85,
right: 20,
backgroundColor: colors.pastelOrange,
width: 60,
height: 60,
borderRadius: 30,
justifyContent: "center",
alignItems: "center",
shadowColor: "#000",
shadowOpacity: 0.2,
shadowRadius: 5,
shadowOffset: { width: 0, height: 3 },
elevation: 5, // for Android shadow
}}
onPress={() => navigation.navigate("YourContacts" as never)}
>
<Ionicons name="person-add" size={28} color={colors.textDark} />
</TouchableOpacity>
) : null
}
{renderContent()} {renderContent()}
</View> </View>
</AppView> </AppView>
@@ -234,59 +164,17 @@ const ManagementScreen: FC = () => {
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: { flex: 1, backgroundColor: '#f5f6fa' },
flex: 1, header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
backgroundColor: '#f5f6fa' title: { fontSize: 22, fontWeight: "600", color: colors.Settings },
}, segmentedControl: { flexDirection: 'row', backgroundColor: '#e9ecef', borderRadius: 10, marginHorizontal: 20, marginBottom: 20, padding: 3 },
header: { segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 },
paddingHorizontal: 20, segmentActive: { backgroundColor: colors.pastelOrange, elevation: 3, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
paddingBottom: 15, segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
}, segmentTextActive: { color: colors.textDark },
title: { contentArea: { flex: 1, paddingHorizontal: 20 },
fontSize: 22, fontWeight: "600", color: colors.Settings loader: { marginTop: 50 },
}, emptyText: { textAlign: 'center', marginTop: 60, fontSize: 16, color: '#6c757d' },
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',
},
}); });
export default ManagementScreen; export default ManagementScreen;
+3 -2
View File
@@ -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 { apiCall } from '../../utils/ApiHelper'; // Adjust path to your apiCall utility
import AppView from '../../components/AppView'; import AppView from '../../components/AppView';
import colors from '../../components/colors'; import colors from '../../components/colors';
import { useRoute } from '@react-navigation/native';
// --- Type Definitions --- // --- Type Definitions ---
interface Member { interface Member {
@@ -38,8 +39,8 @@ interface ApiResponse {
// --- Main Screen Component --- // --- Main Screen Component ---
// This assumes the component receives `groupId` from a navigator (e.g., Tab Navigator) // This assumes the component receives `groupId` from a navigator (e.g., Tab Navigator)
const MemberScreen = () => { const MemberScreen = ({ route }) => {
const route = use
const { groupId } = route.params; const { groupId } = route.params;
const [members, setMembers] = useState<Member[]>([]); const [members, setMembers] = useState<Member[]>([]);
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
+244 -10
View File
@@ -1,12 +1,246 @@
import { View, Text } from 'react-native' import React, { useEffect, useState, useCallback } from 'react';
import React from 'react' import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity } from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
const Payments = () => { import * as Keychain from 'react-native-keychain';
return ( import { Ionicons } from "@react-native-vector-icons/ionicons";
<View> import colors from '../../components/colors'; // Assuming you have a colors file
<Text>Payments</Text> import AppView from '../../components/AppView';
</View> import { apiCall } from '../../utils/ApiHelper';
) interface ApiResponse<T> {
status: 'success' | 'error';
data: T;
message?: string;
statusCode: number;
} }
export default Payments // 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<any>();
const navigation = useNavigation<any>();
const { groupId } = route.params;
const [paymentDetails, setPaymentDetails] = useState<any>(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<PaymentDetails>; // 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) => (
<View key={participant.userId} style={styles.participantCard}>
<View style={styles.participantInfo}>
<Text style={styles.participantName}>{participant.firstName} {participant.lastName}</Text>
<Text style={styles.participantMobile}>{participant.mobile}</Text>
</View>
<View style={styles.paymentStatus}>
<Text style={[
styles.statusText,
{ color: participant.status === 'PENDING' ? colors.warning : colors.success }
]}>
{participant.status}
</Text>
<Text style={styles.amountText}>
₹{participant.paidAmount} / ₹{participant.contributionAmount}
</Text>
{/* {participant.status === 'PENDING' && (
<TouchableOpacity style={styles.payButton}>
<Text style={styles.payButtonText}>Pay Now</Text>
</TouchableOpacity>
)} */}
</View>
</View>
);
if (loading) {
return <ActivityIndicator style={styles.centered} size="large" color={colors.primary} />;
}
if (!paymentDetails) {
return (
<View style={styles.centered}>
<Text>No payment details found.</Text>
<TouchableOpacity onPress={fetchPaymentDetails} style={styles.retryButton}>
<Text style={styles.retryButtonText}>Tap to retry</Text>
</TouchableOpacity>
</View>
);
}
return (
<AppView>
<ScrollView
style={styles.container}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
>
<View style={styles.header}>
<Text style={styles.headerTitle}>{paymentDetails.groupName}</Text>
</View>
<View style={styles.summaryContainer}>
<View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Current Cycle</Text>
<Text style={styles.summaryValue}>{paymentDetails.currentCycle}</Text>
</View>
<View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Contribution</Text>
<Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text>
</View>
<View style={styles.summaryBox}>
<Text style={styles.summaryLabel}>Total Due</Text>
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails.totalDueWithLateFee}</Text>
</View>
</View>
<Text style={styles.sectionTitle}>Participants</Text>
{paymentDetails.participants.map(renderParticipant)}
</ScrollView>
</AppView>
);
};
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;
+67 -45
View File
@@ -8,7 +8,6 @@ import {
ScrollView, ScrollView,
KeyboardAvoidingView, KeyboardAvoidingView,
Platform, Platform,
SafeAreaView,
TouchableOpacity, TouchableOpacity,
} from "react-native"; } from "react-native";
import * as Keychain from "react-native-keychain"; import * as Keychain from "react-native-keychain";
@@ -16,17 +15,17 @@ import { CustomInput, CustomButton } from "../../components/common/Custom";
import colors from "../../components/colors"; import colors from "../../components/colors";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import AppView from "../../components/AppView"; import AppView from "../../components/AppView";
import Ionicons from "@react-native-vector-icons/ionicons"; import { Ionicons } from "@react-native-vector-icons/ionicons";
const BecomeOrg = () => { const BecomeOrg = () => {
const navigation = useNavigation<any>(); const navigation = useNavigation<any>();
const [bankAccountNumber, setBankAccountNumber] = useState(""); const [idName, setIdName] = useState("");
const [bankName, setBankName] = useState(""); const [idNumber, setIdNumber] = useState("");
const [branchCode, setBranchCode] = useState(""); const [issueDate, setIssueDate] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const handleSubmit = async () => { const handleSubmit = async () => {
if (!bankAccountNumber || !bankName || !branchCode) { if (!idName || !idNumber || !issueDate) {
Alert.alert("Error", "Please fill all fields"); Alert.alert("Error", "Please fill all fields");
return; return;
} }
@@ -35,7 +34,10 @@ const BecomeOrg = () => {
setLoading(true); setLoading(true);
const credentials = await Keychain.getGenericPassword(); const credentials = await Keychain.getGenericPassword();
if (!credentials) { 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; return;
} }
const token = credentials.password; const token = credentials.password;
@@ -45,31 +47,31 @@ const BecomeOrg = () => {
{ {
method: "POST", method: "POST",
headers: { headers: {
Accept: "*/*",
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
bankAccountNumber, idName,
bankName, idNumber,
branchCode, issueDate,
}), }),
} }
); );
const result = await response.json(); const result = await response.json();
if (result.status === "success") {
Alert.alert("Success", "Organizer registration applied successfully"); if (response.ok && result.status === "success") {
setBankAccountNumber(""); Alert.alert("Success", "Organizer registration application submitted successfully!");
setBankName(""); setIdName("");
setBranchCode(""); setIdNumber("");
setIssueDate("");
navigation.navigate("Home" as never); navigation.navigate("Home" as never);
} else { } else {
Alert.alert("Error", result.message || "Failed to apply"); Alert.alert("Error", result.message || "Failed to submit application");
} }
} catch (error) { } catch (error) {
console.error("❌ Registration error:", error); console.error("❌ Registration error:", error);
Alert.alert("Error", "Something went wrong"); Alert.alert("Error", "An unexpected error occurred. Please try again.");
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -78,11 +80,11 @@ const BecomeOrg = () => {
return ( return (
<AppView barStyle="dark-content"> <AppView barStyle="dark-content">
<View style={styles.header}> <View style={styles.header}>
<TouchableOpacity onPress={() => navigation.replace("Home" as never)} > <TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
<Ionicons name="arrow-back" size={24} color={'#000'} /> <Ionicons name="arrow-back" size={24} color={'#000'} />
</TouchableOpacity> </TouchableOpacity>
<Text style={styles.headerTitle}>Become Organizer</Text> <Text style={styles.headerTitle}>Become an Organizer</Text>
<View style={styles.placeholder} />
</View> </View>
<KeyboardAvoidingView <KeyboardAvoidingView
style={{ flex: 1 }} style={{ flex: 1 }}
@@ -93,37 +95,42 @@ const BecomeOrg = () => {
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
> >
<Text style={styles.title}>Organizer Registration</Text> <Text style={styles.title}>Submit Your ID for Verification</Text>
<Text style={styles.subtitle}>
Please provide your official identification details to become a verified organizer.
</Text>
<CustomInput <CustomInput
placeholder="Bank Account Number" placeholder="ID Name (e.g., Aadhaar, PAN Card)"
keyboardType="numeric" value={idName}
value={bankAccountNumber} onChangeText={setIdName}
onChangeText={setBankAccountNumber} icon="person-outline"
icon="card-outline" error={""} /> error={""}
/>
<CustomInput <CustomInput
placeholder="Bank Name" placeholder="ID Number"
value={bankName} value={idNumber}
onChangeText={setBankName} onChangeText={setIdNumber}
icon="business-outline" icon="card-outline"
error="" error=""
/> />
<CustomInput <CustomInput
placeholder="Branch Code" placeholder="Issue Date (YYYY-MM-DD)"
value={branchCode} value={issueDate}
onChangeText={setBranchCode} onChangeText={setIssueDate}
icon="key-outline" icon="calendar-outline"
error="" error=""
/> />
<CustomButton <CustomButton
title={loading ? "Submitting..." : "Request Organizer Role"} title={loading ? "Submitting..." : "Request Organizer Role"}
onPress={handleSubmit} onPress={handleSubmit}
icon="send-outline" icon="send-outline"
buttonBackground={colors.pastelBlue} buttonBackground={colors.pastelBlue}
style={{ opacity: loading ? 0.6 : 1 }} style={{ opacity: loading ? 0.6 : 1, marginTop: 20 }}
/> />
{loading && ( {loading && (
@@ -148,23 +155,38 @@ const styles = StyleSheet.create({
justifyContent: "center", justifyContent: "center",
}, },
title: { title: {
fontSize: 22, fontSize: 24,
fontWeight: "700", fontWeight: "700",
marginBottom: 20, marginBottom: 10,
textAlign: "center", textAlign: "center",
color: colors.textDark, color: colors.textDark,
}, },
subtitle: {
fontSize: 16,
textAlign: "center",
color: colors.textDark,
marginBottom: 30,
},
header: { header: {
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between",
alignItems: "center", alignItems: "center",
padding: 16, paddingHorizontal: 16,
borderBottomLeftRadius: 20, paddingVertical: 12,
borderBottomRightRadius: 20, backgroundColor: '#FFF',
borderBottomWidth: 1,
borderBottomColor: '#E0E0E0',
},
backButton: {
padding: 5,
}, },
headerTitle: { headerTitle: {
fontSize: 22, flex: 1,
fontWeight: "700", fontSize: 20,
color: "#000" fontWeight: "600",
color: "#000",
textAlign: "center",
},
placeholder: {
width: 24, // to balance the back button
}, },
}); });
+1
View File
@@ -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 USER_REQUEST = `${BASE_URL}fund/my-requests`
export const GROUP_ALL_MEMBER = `${BASE_URL}fund/group/all/member` export const GROUP_ALL_MEMBER = `${BASE_URL}fund/group/all/member`
export const UPDATE_BANK_URL = `${BASE_URL}user/bank/update`; export const UPDATE_BANK_URL = `${BASE_URL}user/bank/update`;
export const CONTRIBUTE = `${BASE_URL}payments/groups/`;