botton navi...

This commit is contained in:
Abin Toppo
2025-09-24 14:36:44 +05:30
31 changed files with 1622 additions and 412 deletions
+30
View File
@@ -8,6 +8,7 @@
"name": "CircleCollect", "name": "CircleCollect",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/slider": "^5.0.1", "@react-native-community/slider": "^5.0.1",
"@react-native-picker/picker": "^2.11.2", "@react-native-picker/picker": "^2.11.2",
"@react-native-vector-icons/ionicons": "^12.3.0", "@react-native-vector-icons/ionicons": "^12.3.0",
@@ -2739,6 +2740,29 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@react-native-clipboard/clipboard": {
"version": "1.16.3",
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz",
"integrity": "sha512-cMIcvoZKIrShzJHEaHbTAp458R9WOv0fB6UyC7Ek4Qk561Ow/DrzmmJmH/rAZg21Z6ixJ4YSdFDC14crqIBmCQ==",
"license": "MIT",
"workspaces": [
"example"
],
"peerDependencies": {
"react": ">= 16.9.0",
"react-native": ">= 0.61.5",
"react-native-macos": ">= 0.61.0",
"react-native-windows": ">= 0.61.0"
},
"peerDependenciesMeta": {
"react-native-macos": {
"optional": true
},
"react-native-windows": {
"optional": true
}
}
},
"node_modules/@react-native-community/cli": { "node_modules/@react-native-community/cli": {
"version": "20.0.0", "version": "20.0.0",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.0.0.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.0.0.tgz",
@@ -4791,6 +4815,7 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@@ -8078,6 +8103,7 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"devOptional": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/istanbul-lib-coverage": { "node_modules/istanbul-lib-coverage": {
@@ -8834,6 +8860,7 @@
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
@@ -9753,6 +9780,7 @@
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
@@ -14761,6 +14789,7 @@
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/throat": { "node_modules/throat": {
@@ -15219,6 +15248,7 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"devOptional": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
+1
View File
@@ -10,6 +10,7 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/slider": "^5.0.1", "@react-native-community/slider": "^5.0.1",
"@react-native-picker/picker": "^2.11.2", "@react-native-picker/picker": "^2.11.2",
"@react-native-vector-icons/ionicons": "^12.3.0", "@react-native-vector-icons/ionicons": "^12.3.0",
+27 -5
View File
@@ -1,7 +1,8 @@
// src/components/JoinedGroupCard.tsx // src/components/JoinedGroupCard.tsx
import React, { FC } from 'react'; import React, { FC } from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import colors from './colors';
// Update the type to match the full data structure // Update the type to match the full data structure
interface JoinedGroup { interface JoinedGroup {
@@ -23,6 +24,7 @@ interface JoinedGroup {
interface JoinedGroupCardProps { interface JoinedGroupCardProps {
item: JoinedGroup | any; item: JoinedGroup | any;
onViewPress: () => void; // Function prop for the View button
} }
// Helper function to format dates // Helper function to format dates
@@ -58,7 +60,7 @@ const getStatusStyle = (status: JoinedGroup['status']) => {
} }
}; };
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }) => { 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);
@@ -96,11 +98,14 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }) => {
</View> </View>
</View> </View>
{/* Dates */} {/* Footer with Dates and View Button */}
<View style={styles.footer}> <View style={styles.footer}>
<Text style={styles.dateText}> <Text style={styles.dateText}>
{formatDate(item.startDate)} - {formatDate(item.endDate)} {formatDate(item.startDate)} - {formatDate(item.endDate)}
</Text> </Text>
<TouchableOpacity style={styles.viewButton} onPress={onViewPress}>
<Text style={styles.viewButtonText}>View</Text>
</TouchableOpacity>
</View> </View>
</View> </View>
); );
@@ -151,7 +156,7 @@ const styles = StyleSheet.create({
paddingVertical: 16, paddingVertical: 16,
borderTopWidth: 1, borderTopWidth: 1,
borderBottomWidth: 1, borderBottomWidth: 1,
borderColor: '#F3F4F6', borderColor: colors.pastelOrange,
marginBottom: 16, marginBottom: 16,
}, },
infoItem: { infoItem: {
@@ -193,16 +198,33 @@ const styles = StyleSheet.create({
}, },
progressBarFill: { progressBarFill: {
height: '100%', height: '100%',
backgroundColor: '#4F46E5', backgroundColor: colors.Settings,
borderRadius: 4, borderRadius: 4,
}, },
footer: { footer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginTop: 8, marginTop: 8,
borderTopWidth: 1,
borderTopColor: '#F3F4F6',
paddingTop: 16,
}, },
dateText: { dateText: {
fontSize: 14, fontSize: 14,
color: '#4B5563', color: '#4B5563',
fontWeight: '500', fontWeight: '500',
}, },
// --- NEW STYLES for the View Button ---
viewButton: {
backgroundColor: colors.pastelOrange,
paddingHorizontal: 20,
paddingVertical: 10,
borderRadius: 10,
},
viewButtonText: {
color: '#000000ff',
fontSize: 14,
fontWeight: '600',
},
}); });
+1 -1
View File
@@ -50,7 +50,7 @@ export const MyGroupCard: React.FC<Props> = ({ item, onManage }) => {
{item.groupName} {item.groupName}
</Text> </Text>
<View style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}> <View style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}>
<Text style={styles.statusBadgeText}>{item.status === "ACTIVE" ? "Active" : "Not Active"}</Text> <Text style={styles.statusBadgeText}>{item.status}</Text>
</View> </View>
</View> </View>
+1 -10
View File
@@ -50,16 +50,7 @@ export const RequestCard: FC<RequestCardProps> = ({ item, onApprove, onDecline }
); );
}; };
// Assuming a colors object like this for context:
// const colors = {
// white: '#FFFFFF',
// textPrimary: '#212121',
// textSecondary: '#757575',
// border: '#EEEEEE',
// danger: '#C62828',
// lightDanger: '#FFEBEE',
// pastelOrange: '#FFDAB9', // The requested color
// };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
+154
View File
@@ -0,0 +1,154 @@
// src/components/UserRequested.tsx
import React, { FC } from "react";
import { View, Text, StyleSheet } from "react-native";
import {Ionicons} from "@react-native-vector-icons/ionicons";
import colors from "./colors";
import { UserJoinRequest } from "../types";
import { formatDate } from "../utils/HelperFunction";
interface UserRequestedProps {
item: UserJoinRequest;
}
const getStatusStyle = (status: UserJoinRequest["status"]) => {
switch (status) {
case "APPROVED":
return {
backgroundColor: "#D1FAE5", // light green
textColor: "#065F46", // dark green
label: "Approved",
icon: "checkmark-circle-outline",
};
case "PENDING":
return {
backgroundColor: "#FEF3C7", // light yellow
textColor: "#92400E", // dark yellow/brown
label: "Pending",
icon: "time-outline",
};
case "REJECT":
return {
backgroundColor: "#E5E7EB", // light gray
textColor: "#4B5563", // dark gray
label: "Reject",
icon: "close-circle-outline",
};
default:
return {
backgroundColor: "#E5E7EB",
textColor: "#4B5563",
label: "Unknown",
icon: "help-outline",
};
}
};
export const UserRequested: FC<UserRequestedProps> = ({ item }) => {
const { backgroundColor, textColor, label, icon } = getStatusStyle(item.status);
return (
<View style={styles.card}>
{/* Top row: User info + Status */}
<View style={styles.topRow}>
<View style={styles.userInfoSection}>
<Text style={styles.groupName}>{item.groupName}</Text>
<Text style={styles.userMeta}>
<Ionicons name="time-outline" size={14} color={colors.textLight} />{" "}
{formatDate(item.requestDate)}
</Text>
</View>
{/* Status Badge */}
<View style={[styles.statusBadge, { backgroundColor }]}>
<Ionicons name={icon as any} size={16} color={textColor} />
<Text style={[styles.statusText, { color: textColor }]}>{label}</Text>
</View>
</View>
{/* Group Info */}
<View style={styles.groupInfoSection}>
<Text style={styles.requestText}>
Group Created By :{" "}
<Text style={styles.userName} numberOfLines={1}>
{item.createdBy}
</Text>
</Text>
{item.reason ? (
<Text style={styles.reasonText} numberOfLines={2}>
Reason: {item.reason}
</Text>
) : null}
</View>
</View>
);
};
const styles = StyleSheet.create({
card: {
backgroundColor: colors.white,
borderRadius: 16,
marginVertical: 8,
elevation: 3,
shadowColor: "#000",
shadowOpacity: 0.1,
shadowRadius: 6,
shadowOffset: { width: 0, height: 3 },
padding: 12,
},
topRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
borderBottomWidth: 1,
borderBottomColor: colors.Settings,
paddingBottom: 8,
},
userInfoSection: {
flex: 1,
paddingRight: 8,
},
userName: {
// fontSize: 16,
fontWeight: "bold",
color: colors.textDark,
},
userMeta: {
fontSize: 13,
color: colors.textLight,
marginTop: 2,
},
groupInfoSection: {
paddingVertical: 12,
},
requestText: {
// fontSize: 15,
color: colors.textDark,
lineHeight: 20,
},
groupName: {
fontWeight: "600",
color: colors.Settings,
fontSize:18
},
reasonText: {
fontSize: 13,
color: colors.textLight,
marginTop: 4,
},
statusBadge: {
flexDirection: "row",
alignItems: "center",
borderRadius: 12,
paddingHorizontal: 8,
paddingVertical: 4,
},
statusText: {
fontSize: 13,
fontWeight: "600",
marginLeft: 4,
},
});
+15 -16
View File
@@ -1,18 +1,24 @@
import React from "react"; import React from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native"; import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import { Ionicons } from "@react-native-vector-icons/ionicons" import {Ionicons} from "@react-native-vector-icons/ionicons";
type HeaderProps = { type HeaderProps = {
title: string; title: string;
showBack?: boolean; // optional, default true showBack?: boolean; // optional, default true
onBackPress?: () => void; // optional custom back action onBackPress?: () => void; // optional custom back action
rightAction?: React.ReactNode; // allow passing JSX for right side
titleColor?:string;
arrowColor?:string
}; };
const Header: React.FC<HeaderProps> = ({ const Header: React.FC<HeaderProps> = ({
title, title,
showBack = true, showBack = true,
onBackPress, onBackPress,
rightAction,
titleColor = 'pink',
arrowColor='pink'
}) => { }) => {
const navigation = useNavigation(); const navigation = useNavigation();
@@ -26,12 +32,15 @@ const Header: React.FC<HeaderProps> = ({
return ( return (
<View style={styles.header}> <View style={styles.header}>
{showBack && ( {showBack ? (
<TouchableOpacity onPress={handleBack} style={styles.backButton}> <TouchableOpacity onPress={handleBack}>
<Ionicons name="arrow-back" size={24} color="#000" /> <Ionicons name="arrow-back" size={24} color={arrowColor} />
</TouchableOpacity> </TouchableOpacity>
) : (
<View style={{ width: 24 }} /> // placeholder to keep spacing
)} )}
<Text style={styles.headerTitle}>{title}</Text> <Text style={[styles.headerTitle, {color:titleColor}]}>{title}</Text>
<View>{rightAction}</View>
</View> </View>
); );
}; };
@@ -42,21 +51,11 @@ const styles = StyleSheet.create({
header: { header: {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
paddingHorizontal: 16, justifyContent: "space-between",
paddingVertical: 12, paddingVertical: 12,
backgroundColor: "#fff",
shadowColor: "#000",
shadowOpacity: 0.05,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 6,
elevation: 3,
},
backButton: {
marginRight: 12,
}, },
headerTitle: { headerTitle: {
fontSize: 18, fontSize: 18,
fontWeight: "600", fontWeight: "600",
color: "#000",
}, },
}); });
+26 -3
View File
@@ -15,6 +15,9 @@ import Transaction from "../screens/profile/Transaction";
import GroupDetails from "../screens/home/user-home/GroupDetails"; import GroupDetails from "../screens/home/user-home/GroupDetails";
import EditGroupScreen from "../screens/management/EditGroupScreen"; import EditGroupScreen from "../screens/management/EditGroupScreen";
import EditBankDetails from "../screens/profile/EditBankDetails"; import EditBankDetails from "../screens/profile/EditBankDetails";
import ManageJoinedGroup from "../screens/management/ManageJoinedGroup";
import InviteScreen from "../screens/InviteScreen";
import ManagementTabNavigation from "../screens/management/ManagementTabNavigation";
export type RootStackParamList = { export type RootStackParamList = {
Splash: undefined; Splash: undefined;
@@ -32,6 +35,9 @@ export type RootStackParamList = {
YourContacts: undefined; YourContacts: undefined;
Transaction: undefined; Transaction: undefined;
EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null }; EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null };
ManageJoinedGroup: { groupId: string | number };
Invite: { groupId: string | number; code: string | number };
ManagementTabs:any
}; };
const Stack = createNativeStackNavigator<RootStackParamList>(); const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -39,16 +45,30 @@ const Stack = createNativeStackNavigator<RootStackParamList>();
export default function AppNavigator() { export default function AppNavigator() {
const linking = { const linking = {
prefixes: [ prefixes: [
"myapp://", // A custom scheme for internal or test links. "myapp://", // Your custom scheme
"https://192.168.1.19" // Your production domain for verified App Links. "https://your-production-domain.com" // Your website for Universal Links/App Links
], ],
config: { config: {
screens: { screens: {
GroupDetails: "invite/:inviteCode", // Define the route for the InviteScreen
Invite: {
// This path matches the part after the "://" and before the "?"
path: "invite",
// This block tells React Navigation how to handle the query parameters
parse: {
groupId: (groupId: string | number) => `${groupId}`,
code: (code: string | number) => `${code}`,
},
},
// Other screens in your navigator can be defined here
// e.g., GroupDetails: "group/:id"
}, },
}, },
}; };
return ( return (
<NavigationContainer linking={linking}> <NavigationContainer linking={linking}>
<Stack.Navigator initialRouteName="Splash"> <Stack.Navigator initialRouteName="Splash">
@@ -66,6 +86,9 @@ export default function AppNavigator() {
<Stack.Screen name="GroupDetails" component={GroupDetails} options={{ headerShown: false }} /> <Stack.Screen name="GroupDetails" component={GroupDetails} options={{ headerShown: false }} />
<Stack.Screen name="EditGroup" component={EditGroupScreen} options={{ headerShown: false }} /> <Stack.Screen name="EditGroup" component={EditGroupScreen} options={{ headerShown: false }} />
<Stack.Screen name="EditBankDetails" component={EditBankDetails} options={{ headerShown: false }} /> <Stack.Screen name="EditBankDetails" component={EditBankDetails} options={{ headerShown: false }} />
<Stack.Screen name="ManageJoinedGroup" component={ManageJoinedGroup} options={{ headerShown: false }} />
<Stack.Screen name="Invite" component={InviteScreen} options={{ headerShown: false }} />
<Stack.Screen name="ManagementTabs" component={ManagementTabNavigation} options={{ headerShown: false }} />
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
+2 -2
View File
@@ -39,9 +39,9 @@ export default function GroupCreationForm() {
} }
}; };
const cycleTypes = ["CUSTOM", "MONTHLY", "QUARTERLY", "YEARLY"]; const cycleTypes = ["CUSTOM", "MONTHLY", "QUARTERLY", "YEARLY", "HALF_YEARLY"];
const distributionTypes = ["SEQUENTIAL", "RANDOM"]; const distributionTypes = ["SEQUENTIAL", "RANDOM"];
const lateFeeModes = ["DAILY", "FIXED"]; const lateFeeModes = ["DAILY", "MONTHLY"];
const validateForm = () => { const validateForm = () => {
const newErrors: any = {}; const newErrors: any = {};
+36 -11
View File
@@ -21,7 +21,7 @@ import { StackNavigationProp } from "@react-navigation/stack";
import { RootStackParamList } from "../navigation/AppNavigator"; import { RootStackParamList } from "../navigation/AppNavigator";
import AppView from "../components/AppView"; import AppView from "../components/AppView";
import { apiCall } from "../utils/ApiHelper"; import { apiCall } from "../utils/ApiHelper";
import { BASE_URL, JOIN_GROUP_URL, PUBLIC_GROUPS_URL, PUT_USER_STATUS } from "../utils/ApiUrls"; import { BASE_URL, GET_JOINED_GROUP_URL, JOIN_GROUP_URL, PUBLIC_GROUPS_URL, PUT_USER_STATUS } from "../utils/ApiUrls";
import colors from "../components/colors"; import colors from "../components/colors";
import { ApiResponse } from "./home/UserProfileStatus"; import { ApiResponse } from "./home/UserProfileStatus";
import * as Keychain from "react-native-keychain"; import * as Keychain from "react-native-keychain";
@@ -114,7 +114,31 @@ export default function GroupScreen() {
} }
}; };
useFocusEffect(React.useCallback(() => { fetchGroups(); }, [])); const fetchJoinedGroups = async () => {
if (loading) return;
setLoading(true);
setError(null);
try {
console.log("first-????-")
const res = await apiCall(GET_JOINED_GROUP_URL, "GET") as { data?: Group[] };
console.log("first---", res)
if (res && res.data) {
setJoinedGroups(res.data);
} else {
throw new Error("No data received");
}
} catch (e) {
setError("Failed to fetch groups. Please try again.");
} finally {
setLoading(false);
}
};
useFocusEffect(React.useCallback(() => {
fetchGroups()
fetchJoinedGroups()
}, []));
const filterData = (data: Group[]) => { const filterData = (data: Group[]) => {
if (!searchQuery.trim()) return data; if (!searchQuery.trim()) return data;
@@ -126,22 +150,23 @@ export default function GroupScreen() {
const handleJoinGroup = async (groupId: string, userId: string) => { const handleJoinGroup = async (groupId: string, userId: string) => {
console.log("Joining group:", groupId, "User:", userId); console.log("Joining group:", groupId, "User:", userId);
try { try {
const url = `${JOIN_GROUP_URL}/groups/${groupId}/join/0`; const url = `${JOIN_GROUP_URL}groups/${groupId}/join/10`;
console.log("Joining group URL:", url); console.log("Joining group URL:", url);
const res = await apiCall(url, "POST") as { statusCode?: number; message?: string }; const res = await apiCall(url, "POST");
console.log("JOIN_GROUP_URL response:", res); console.log("JOIN_GROUP_URL response:", res);
if (res.statusCode === 200) { // if (res.statusCode === 200) {
Alert.alert("Success", "You requested to join the group."); // Alert.alert("Success", "You requested to join the group.");
} else { // } else {
Alert.alert("Error", res.message || "Failed to join group."); // Alert.alert("Error", res.message || "Failed to join group.");
} // }
} catch (error) { } catch (error) {
console.error("Join Group Error:", error); console.error("Join Group Error:", error);
Alert.alert("Error", "An unexpected error occurred."); Alert.alert("Error", "An unexpected error occurred.");
} }
}; };
const userStatus = async () => { const userStatus = async () => {
try { try {
const credentials = await Keychain.getGenericPassword(); const credentials = await Keychain.getGenericPassword();
@@ -239,7 +264,7 @@ export default function GroupScreen() {
refreshControl={ refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} /> <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
} }
ListFooterComponent={<ActivityIndicator size={'large'} color={colors.Group}/>} // ListFooterComponent={<ActivityIndicator size={'large'} color={colors.Group} />}
/> />
); );
}; };
@@ -265,7 +290,7 @@ export default function GroupScreen() {
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
renderItem={({ item }) => <FeaturedGroupCard item={item} />} renderItem={({ item }) => <FeaturedGroupCard item={item} />}
style={styles.featuredList} style={styles.featuredList}
ListEmptyComponent={!loading && featuredGroups.length === 0 ? <Text style={styles.emptyFeaturedText}>No featured groups.</Text> : null} // ListEmptyComponent={!loading && featuredGroups.length === 0 ? <Text style={styles.emptyFeaturedText}>No featured groups.</Text> : null}
/> />
<View style={styles.tabHeader}> <View style={styles.tabHeader}>
+205
View File
@@ -0,0 +1,205 @@
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import React, { useState, useEffect, useCallback, FC } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Alert,
ActivityIndicator,
} from 'react-native';
import { RootStackParamList } from '../navigation/AppNavigator'; // Adjust path
import { apiCall } from '../utils/ApiHelper'; // Adjust path
import AppView from '../components/AppView'; // Adjust path
import colors from '../components/colors'; // Adjust path
// --- Type Definitions ---
interface Participant {
id: string;
firstName: string;
lastName: string | null;
mobile: string;
role: 'ORGANIZER' | 'USER';
}
interface GroupDetails {
id: string;
groupName: string;
description: string;
contributionAmount: number;
maxParticipants: number;
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'APPROVED';
organizerFirstName: string;
organizerLastName: string | null;
totalParticipants: number;
participants: Participant[];
}
type InviteScreenProps = NativeStackScreenProps<RootStackParamList, 'Invite'>; // Assuming 'Invite' is in your stack
// --- Main Screen Component ---
const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
const { groupId,code} = route.params;
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
const [loading, setLoading] = useState(true);
const [joining, setJoining] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchGroupDetails = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
if (response.status === 'success' && response.data) {
setGroupDetails(response.data);
} else {
throw new Error(response.message || 'Failed to fetch group invitation details.');
}
} catch (e: any) {
setError(e.message);
} finally {
setLoading(false);
}
}, [groupId]);
useEffect(() => {
fetchGroupDetails();
}, [fetchGroupDetails]);
const handleJoinGroup = async () => {
if (!groupDetails) return;
setJoining(true);
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 });
Alert.alert('Success!', `You have requested to join "${groupDetails.groupName}".`);
} catch (e: any) {
Alert.alert('Join Error', e.message || 'An unexpected error occurred.');
} finally {
setJoining(false);
}
};
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color={colors.primary} /></View>;
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
if (!groupDetails) return null;
const organizerName = `${groupDetails.organizerFirstName} ${groupDetails.organizerLastName || ''}`.trim();
return (
<AppView>
<ScrollView contentContainerStyle={styles.contentContainer}>
<Text style={styles.headerTitle}>You're Invited!</Text>
<Text style={styles.headerSubtitle}>Join the group <Text style={{ fontWeight: 'bold' }}>{groupDetails.groupName}</Text></Text>
{/* Group Info Card */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Group Details</Text>
<InfoRow label="Organizer" value={organizerName} />
<InfoRow label="Contribution" value={`₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`} />
<InfoRow label="Payment Cycle" value={groupDetails.cycleType} />
<InfoRow label="Status" value={groupDetails.status} />
</View>
{/* Participants Card */}
<View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants} / {groupDetails.maxParticipants})</Text>
{groupDetails.participants.length > 0 ? (
groupDetails.participants.map(p => (
<View key={p.id} style={styles.participantRow}>
<Text style={styles.participantName}>{`${p.firstName} ${p.lastName || ''}`.trim()}</Text>
{p.role === 'ORGANIZER' && <View style={styles.organizerBadge}><Text style={styles.organizerBadgeText}>Organizer</Text></View>}
</View>
))
) : (
<Text style={styles.noParticipantsText}>Be the first to join!</Text>
)}
</View>
</ScrollView>
{/* Floating Join Button */}
<View style={styles.floatingButtonContainer}>
<TouchableOpacity
style={[styles.joinButton, groupDetails.status !== 'PENDING' && groupDetails.status !== 'APPROVED' ? styles.disabledButton : {}]}
onPress={handleJoinGroup}
disabled={joining || (groupDetails.status !== 'PENDING' && groupDetails.status !== 'APPROVED')}
>
{joining ? (
<ActivityIndicator color="#FFFFFF" />
) : (
<Text style={styles.joinButtonText}>
{groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED' ? 'Join Group' : `Group is ${groupDetails.status}`}
</Text>
)}
</TouchableOpacity>
</View>
</AppView>
);
};
// Helper component for displaying rows
const InfoRow = ({ label, value }: { label: string; value: string }) => (
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}</Text>
<Text style={styles.infoValue}>{value}</Text>
</View>
);
const styles = StyleSheet.create({
centered: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F3F4F6' },
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center', padding: 20 },
contentContainer: { padding: 16, paddingBottom: 100 }, // Add padding to avoid overlap with button
headerTitle: { fontSize: 32, fontWeight: 'bold', color: '#111827', textAlign: 'center' },
headerSubtitle: { fontSize: 18, color: '#4B5563', textAlign: 'center', marginBottom: 24, marginTop: 4 },
card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 },
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937', marginBottom: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10 },
infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 },
infoLabel: { fontSize: 16, color: '#6B7280' },
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10 },
participantName: { fontSize: 16, color: '#1F2937' },
organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12 },
organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' },
noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 10 },
floatingButtonContainer: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
alignItems: 'center',
},
joinButton: {
backgroundColor: colors.primary,
borderRadius: 14,
paddingVertical: 16,
paddingHorizontal: 32,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 4,
width: '80%',
alignItems: 'center',
},
joinButtonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: 'bold',
},
disabledButton: {
backgroundColor: '#9CA3AF', // Gray color when disabled
},
});
export default InviteScreen;
+1 -22
View File
@@ -19,7 +19,6 @@ import type { StackNavigationProp } from "@react-navigation/stack";
import { RootStackParamList } from "../navigation/AppNavigator"; import { RootStackParamList } from "../navigation/AppNavigator";
import { CustomButton, CustomInput } from "../components/common/Custom"; import { CustomButton, CustomInput } from "../components/common/Custom";
import colors from "../components/colors"; import colors from "../components/colors";
import { useSelector } from "react-redux";
const theme = { const theme = {
primary: "#111111", primary: "#111111",
@@ -42,8 +41,7 @@ export default function LoginScreen() {
const fade = useRef(new Animated.Value(0)).current; const fade = useRef(new Animated.Value(0)).current;
const slide = useRef(new Animated.Value(24)).current; const slide = useRef(new Animated.Value(24)).current;
const ctaScale = useRef(new Animated.Value(1)).current; const ctaScale = useRef(new Animated.Value(1)).current;
const user = useSelector((state: any) => state.user)
console.log("current user ----", user)
useEffect(() => { useEffect(() => {
Animated.parallel([ Animated.parallel([
Animated.timing(fade, { toValue: 1, duration: 600, useNativeDriver: true }), Animated.timing(fade, { toValue: 1, duration: 600, useNativeDriver: true }),
@@ -55,8 +53,6 @@ export default function LoginScreen() {
if (mobile.length === 10) { if (mobile.length === 10) {
setLoading(true); setLoading(true);
try { try {
console.log("LOGIN_URL console:", LOGIN_URL, "mobile:", mobile);
const res = await axios.get(LOGIN_URL, { const res = await axios.get(LOGIN_URL, {
params: { mobile }, params: { mobile },
}); });
@@ -89,23 +85,6 @@ export default function LoginScreen() {
} }
}; };
const onPressIn = () =>
Animated.spring(ctaScale, {
toValue: 0.97,
useNativeDriver: true,
speed: 20,
bounciness: 0,
}).start();
const onPressOut = () =>
Animated.spring(ctaScale, {
toValue: 1,
useNativeDriver: true,
speed: 20,
bounciness: 6,
}).start();
return ( return (
<KeyboardAvoidingView <KeyboardAvoidingView
style={styles.container} style={styles.container}
+20 -10
View File
@@ -19,10 +19,7 @@ import {
import { RootStackParamList } from "../navigation/AppNavigator"; import { RootStackParamList } from "../navigation/AppNavigator";
import * as Keychain from "react-native-keychain"; import * as Keychain from "react-native-keychain";
import { BASE_URL, LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls"; import { BASE_URL, LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls";
import { StackNavigationProp } from "@react-navigation/stack";
import colors from "../components/colors"; import colors from "../components/colors";
import { useDispatch } from "react-redux";
import { setUserData } from "../redux/userSlice";
import { fetchUserProfile } from "../utils/HelperFunction"; import { fetchUserProfile } from "../utils/HelperFunction";
const theme = { const theme = {
@@ -44,7 +41,7 @@ export default function OtpVerificationScreen() {
const inputs = useRef<Array<TextInput | null>>([]); const inputs = useRef<Array<TextInput | null>>([]);
const route = useRoute<OtpVerifyRouteProp>(); const route = useRoute<OtpVerifyRouteProp>();
const { mobile } = route.params; const { mobile } = route.params;
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>(); const navigation = useNavigation<any>();
// animations // animations
const fade = useRef(new Animated.Value(0)).current; const fade = useRef(new Animated.Value(0)).current;
const slide = useRef(new Animated.Value(24)).current; const slide = useRef(new Animated.Value(24)).current;
@@ -85,22 +82,35 @@ export default function OtpVerificationScreen() {
const res = await axios.post( const res = await axios.post(
VARIFY_OTP_URL, VARIFY_OTP_URL,
payload, payload,
{ headers: { "Content-Type": "application/json", Accept: "*/*" } } {
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
}
); );
console.log("✅ OTP Verified:", res.data); console.log("✅ OTP Verified:", res.data);
if (res.data.statusCode === 200) {
saveToken(res.data.data.jwt); if (res.data?.statusCode === 200) {
fetchUserProfile() const { jwt, role } = res.data?.data || {};
navigation.navigate('Home')
saveToken(jwt);
navigation.navigate("Home");
ToastAndroid.show("OTP Verified Successfully!", ToastAndroid.SHORT); ToastAndroid.show("OTP Verified Successfully!", ToastAndroid.SHORT);
} }
} catch (err: any) { } catch (err: any) {
console.error("❌ OTP Verification Error:", err?.response?.data || err.message); console.error("❌ OTP Verification Error:", err?.response?.data || err.message);
ToastAndroid.show(err.message || "Invalid OTP. Please try again.", ToastAndroid.SHORT); ToastAndroid.show(
err?.response?.data?.message || "Invalid OTP. Please try again.",
ToastAndroid.SHORT
);
} }
}; };
const resendOtp = async () => { const resendOtp = async () => {
try { try {
setIsResending(true); setIsResending(true);
+3 -1
View File
@@ -12,6 +12,8 @@ import { StackNavigationProp } from "@react-navigation/stack";
const SplashScreen = () => { const SplashScreen = () => {
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>(); const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
const dispatch = useDispatch(); const dispatch = useDispatch();
const fadeAnim = useRef(new Animated.Value(1)).current; const fadeAnim = useRef(new Animated.Value(1)).current;
@@ -101,7 +103,7 @@ const SplashScreen = () => {
}; };
startSplash(); startSplash();
}, [navigation, fadeAnim, dispatch]); }, [navigation, fadeAnim, dispatch, ]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
+83 -9
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { import {
StyleSheet, StyleSheet,
Text, Text,
@@ -7,33 +7,108 @@ import {
UIManager, UIManager,
Platform, Platform,
Image, Image,
Alert,
} from "react-native"; } from "react-native";
import AppView from "../../components/AppView"; import AppView from "../../components/AppView";
import UserProfileStatus from "./UserProfileStatus"; import UserProfileStatus from "./UserProfileStatus";
import colors from "../../components/colors"; import colors from "../../components/colors";
import GroupsTabs from "./GroupsTabs"; import GroupsTabs from "./GroupsTabs";
import { useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigation } from "@react-navigation/native"; import { RouteProp, useFocusEffect, useNavigation, useRoute } from "@react-navigation/native";
import UserHome from "./user-home/UserHome"; import UserHome from "./user-home/UserHome";
import { PROFILE_URL } from "../../utils/ApiUrls";
import *as Keychain from "react-native-keychain"
import { setUserData } from "../../redux/userSlice";
// Enable animation for Android // Enable animation for Android
if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) { if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true); UIManager.setLayoutAnimationEnabledExperimental(true);
} }
type RootStackParamList = {
Home: { refreshProfile: boolean; role: string };
};
type UserProfile = {
id: string;
userId: number;
firstName: string | null;
lastName: string | null;
imageBlob: string | null;
role?: string;
}
const Home = () => { const Home = () => {
const user = useSelector((state: any) => state.user); const user = useSelector((state: any) => state.user);
const navigation = useNavigation(); const navigation = useNavigation();
console.log("redux user data in Home:", user); const dispatch = useDispatch()
const [apiUser, setApiUser] = useState<UserProfile | null | any>(null)
const fetchUserProfile = async () => {
try {
const credentials = await Keychain.getGenericPassword();
if (!credentials) {
return;
}
const token = credentials.password;
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,
})
);
}
} catch (error) {
console.error("❌ Error fetching profile:", error);
}
};
useEffect(() => {
fetchUserProfile();
}, []);
return ( return (
<AppView barStyle="dark-content" style={styles.container}> <AppView barStyle="dark-content" style={styles.container}>
{/* Header */} {/* Header */}
<View style={styles.header}> <View style={styles.header}>
<Text style={styles.greeting}>Hello, {user.firstName} {user.lastName}👋</Text> <Text style={styles.greeting}>Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}👋</Text>
<TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}> <TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}>
<Image <Image
source={ source={
user.imageBlob user.imageBlob
&& { uri: user.imageBlob ? `data:image/jpeg;base64,${user.imageBlob}` : user.profilePicture } && { 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 }} style={{ width: 40, height: 40, borderRadius: 20, borderColor: colors.primary, borderWidth: 2 }}
/> />
@@ -41,7 +116,8 @@ const Home = () => {
</View> </View>
{/* Status */} {/* Status */}
<UserProfileStatus /> <UserProfileStatus />
{user?.role === "USER" ? <UserHome /> : {/* <UserHome /> */}
{user?.role === "USER" || apiUser?.role === "USER" ? <UserHome /> :
<GroupsTabs />} <GroupsTabs />}
</AppView> </AppView>
); );
@@ -60,6 +136,4 @@ const styles = StyleSheet.create({
marginBottom: 12, marginBottom: 12,
}, },
greeting: { fontSize: 22, fontWeight: "600", color: colors.Home }, greeting: { fontSize: 22, fontWeight: "600", color: colors.Home },
}); });
+12 -4
View File
@@ -13,6 +13,7 @@ import {
import { Ionicons } from '@react-native-vector-icons/ionicons'; import { Ionicons } from '@react-native-vector-icons/ionicons';
import colors from '../../components/colors'; import colors from '../../components/colors';
import DeclineModal from './DeclineModal'; import DeclineModal from './DeclineModal';
import * as keychain from 'react-native-keychain'
// Define TypeScript interfaces // Define TypeScript interfaces
interface GroupRequest { interface GroupRequest {
@@ -45,17 +46,24 @@ const RequestList: React.FC = () => {
// Fetch data from API // Fetch data from API
const fetchRequests = async (): Promise<void> => { const fetchRequests = async (): Promise<void> => {
const credentials = await keychain.getGenericPassword();
if (!credentials) {
Alert.alert("Error", "You are not logged in");
return;
}
const token = credentials.password;
try { try {
setRefreshing(true); setRefreshing(true);
setError(null); setError(null);
const response = await fetch( const response = await fetch(
"https://api.godemo.in/FundCollect/api/fund/group/pending/request", "https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/pending/request",
{ {
method: "GET", method: "GET",
headers: { headers: {
accept: "*/*", accept: "*/*",
Authorization: "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MjY1MjI5MzcxIiwidXNlcklkIjoiMTAwMDAxIiwiaWF0IjoxNzU4MDkxMjEzLCJleHAiOjE3NTgxNzc2MTN9.T3BvU7nl8fnjwwVobIWRmSFFxQzOjtyIct1Ba0i9lIU" Authorization: `Bearer ${token}`
} }
} }
); );
@@ -116,7 +124,7 @@ const RequestList: React.FC = () => {
// Make API call to approve the request // Make API call to approve the request
const response = await fetch( const response = await fetch(
`https://api.godemo.in/FundCollect/api/fund/requests/${requestId}/approve`, `https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/requests/${requestId}/approve`,
{ {
method: "PUT", method: "PUT",
headers: { headers: {
@@ -162,7 +170,7 @@ const RequestList: React.FC = () => {
// Make API call to decline the request // Make API call to decline the request
const response = await fetch( const response = await fetch(
`https://api.godemo.in/FundCollect/api/fund/requests/${requestedId}/reject?reason=/${reason}`, `https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/requests/${requestedId}/reject?reason=/${reason}`,
{ {
method: "PUT", method: "PUT",
headers: { headers: {
+2 -2
View File
@@ -10,7 +10,7 @@ import {
} from "react-native"; } from "react-native";
import * as Keychain from "react-native-keychain"; import * as Keychain from "react-native-keychain";
import { useFocusEffect, useNavigation } from "@react-navigation/native"; import { useFocusEffect, useNavigation } from "@react-navigation/native";
import { BASE_URL, PUT_USER_STATUS } from "../../utils/ApiUrls"; import { PUT_USER_STATUS } from "../../utils/ApiUrls";
import colors from "../../components/colors"; import colors from "../../components/colors";
export type ApiResponse = { export type ApiResponse = {
@@ -54,7 +54,7 @@ const UserProfileStatus: React.FC = () => {
const json: ApiResponse = await response.json(); const json: ApiResponse = await response.json();
console.log("👤 Profile Status Response:", json); console.log("Profile Status :", json);
if (json.status === "success") { if (json.status === "success") {
setStatusData(json.data); setStatusData(json.data);
+3 -1
View File
@@ -11,6 +11,7 @@ import { useRoute } 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";
const GroupDetails = () => { const GroupDetails = () => {
const route = useRoute(); const route = useRoute();
@@ -88,7 +89,8 @@ const GroupDetails = () => {
return ( return (
<AppView> <AppView>
{/* Group Title */} {/* Group Title */}
<Text style={styles.groupName}>{group.name}</Text> <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 */}
+85 -44
View File
@@ -1,65 +1,108 @@
import React from "react"; import React, { useEffect, useState } from "react";
import { import {
StyleSheet, StyleSheet,
Text, Text,
View, View,
FlatList, FlatList,
TouchableOpacity, TouchableOpacity,
Alert,
} 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";
import type { RootStackParamList } from "../../../navigation/AppNavigator"; // adjust path import type { RootStackParamList } from "../../../navigation/AppNavigator";
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 { GET_JOINED_GROUP_URL } from "../../../utils/ApiUrls";
import * as Keychain from "react-native-keychain";
type NavigationProp = NativeStackNavigationProp<RootStackParamList, "GroupDetails">; type NavigationProp = NativeStackNavigationProp<
RootStackParamList,
"GroupDetails"
>;
type Group = { type Group = {
id: string; id: string;
name: string; groupName: string;
members: number; description: string;
collected: number; currentParticipants: number;
goal: number; maxParticipants: number;
contributionAmount: number;
status: string;
}; };
const groups: Group[] = [
{ id: "1", name: "Wedding Fund", members: 25, collected: 12000, goal: 20000 },
{ id: "2", name: "Birthday Gift", members: 10, collected: 4500, goal: 6000 },
{ id: "3", name: "Community Event", members: 50, collected: 22000, goal: 30000 },
];
const UserHome = () => { const UserHome = () => {
const navigation = useNavigation<NavigationProp>(); const navigation = useNavigation<NavigationProp>();
const [groups, setGroups] = useState<Group[]>([]);
const [loading, setLoading] = useState<boolean>(false)
const totalGroups = groups.length; const totalGroups = groups.length;
const totalCollected = groups.reduce((sum, g) => sum + g.collected, 0); const totalContribution = groups.reduce(
(sum, g) => sum + g.contributionAmount,
0
);
const handleGroup = async () => {
try {
setLoading(true)
const credentials = await Keychain.getGenericPassword();
if (!credentials) {
Alert.alert("Error", "You are not logged in");
return;
}
const token = credentials.password;
const response = await fetch(GET_JOINED_GROUP_URL, {
method: "GET",
headers: {
Accept: "*/*",
Authorization: `Bearer ${token}`,
},
});
const result = await response.json();
if (result.status === "success") {
console.log("GET_JOINED_GROUP_URL res : ", result);
setGroups(result.data); // save API data
setLoading(false)
} else {
Alert.alert("Error", "Failed to fetch groups");
}
} catch (error) {
console.error("❌ Error fetching groups:", error);
Alert.alert("Error", "Could not connect to server");
}finally{
setLoading(false)
}
};
useEffect(() => {
handleGroup();
}, []);
const renderItem = ({ item }: { item: Group }) => { const renderItem = ({ item }: { item: Group }) => {
const progress = Math.min(item.collected / item.goal, 1) * 100;
return ( return (
<TouchableOpacity <TouchableOpacity
style={styles.card} style={styles.card}
onPress={() => navigation.navigate("GroupDetails", { groupId: item.id })} onPress={() => navigation.navigate("GroupDetails", { groupId: item.id })}
> >
<Text style={styles.groupName}>{item.name}</Text> <Text style={styles.groupName}>{item.groupName}</Text>
<Text style={styles.description}>{item.description}</Text>
<View style={styles.statsRow}> <View style={styles.statsRow}>
<Ionicons <Ionicons
name="person-circle-outline" name="person-circle-outline"
size={20} size={20}
color={colors.primary} color={colors.primary}
/> />
<Text style={styles.detail}> {item.members} members</Text> <Text style={styles.detail}>
</View> {item.currentParticipants}/{item.maxParticipants} members
</Text>
{/* Progress Bar */}
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${progress}%` }]} />
</View> </View>
<Text style={styles.amount}> <Text style={styles.amount}>
₹{item.collected} / ₹{item.goal} Contribution: ₹{item.contributionAmount}
</Text> </Text>
<Text style={styles.status}>Status: {item.status}</Text>
</TouchableOpacity> </TouchableOpacity>
); );
}; };
@@ -67,14 +110,14 @@ const UserHome = () => {
return ( return (
<View> <View>
{/* Stats */} {/* Stats */}
<View style={[styles.statsRow, { marginVertical: 24, }]}> <View style={[styles.statsRow, { marginVertical: 24 }]}>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statValue}>{totalGroups}</Text> <Text style={styles.statValue}>{totalGroups}</Text>
<Text style={styles.statLabel}>Groups Joined</Text> <Text style={styles.statLabel}>Groups Joined</Text>
</View> </View>
<View style={styles.statBox}> <View style={styles.statBox}>
<Text style={styles.statValue}>₹{totalCollected}</Text> <Text style={styles.statValue}>₹{totalContribution}</Text>
<Text style={styles.statLabel}>Total Contributed</Text> <Text style={styles.statLabel}>Total Contribution</Text>
</View> </View>
</View> </View>
@@ -93,16 +136,13 @@ const UserHome = () => {
export default UserHome; export default UserHome;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
statsRow: { statsRow: {
flexDirection: "row", flexDirection: "row",
gap: 10 gap: 10,
}, },
statBox: { statBox: {
flex: 1, flex: 1,
backgroundColor: "#fff", backgroundColor: "#fff",
// marginHorizontal: 6,
padding: 16, padding: 16,
borderRadius: 12, borderRadius: 12,
alignItems: "center", alignItems: "center",
@@ -143,26 +183,27 @@ const styles = StyleSheet.create({
fontSize: 16, fontSize: 16,
fontWeight: "600", fontWeight: "600",
color: colors.Home, color: colors.Home,
marginBottom: 6, marginBottom: 4,
},
description: {
fontSize: 13,
color: "#475569",
marginBottom: 8,
}, },
detail: { detail: {
fontSize: 13, fontSize: 13,
color: "#475569", color: "#475569",
marginBottom: 8, marginBottom: 8,
}, },
progressBar: {
height: 6,
borderRadius: 3,
backgroundColor: "#e2e8f0",
marginBottom: 6,
overflow: "hidden",
},
progressFill: {
backgroundColor: colors.primary,
height: "100%",
},
amount: { amount: {
fontSize: 13, fontSize: 13,
color: "#64748b", color: "#64748b",
marginTop: 4,
},
status: {
fontSize: 12,
fontWeight: "500",
color: colors.primary,
marginTop: 2,
}, },
}); });
+235 -116
View File
@@ -9,11 +9,17 @@ import {
TouchableOpacity, TouchableOpacity,
Alert, Alert,
ActivityIndicator, ActivityIndicator,
Share,
} from 'react-native'; } from 'react-native';
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
import { apiCall } from '../../utils/ApiHelper'; // Adjust path import { apiCall } from '../../utils/ApiHelper'; // Adjust path
import AppView from '../../components/AppView'; import AppView from '../../components/AppView';
import { Picker } from '@react-native-picker/picker'; import { Picker } from '@react-native-picker/picker';
import colors from '../../components/colors';
import Clipboard from '@react-native-clipboard/clipboard';
import { CompositeScreenProps } from '@react-navigation/native';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path
// --- Type Definitions --- // --- Type Definitions ---
interface Participant { interface Participant {
@@ -35,6 +41,8 @@ export interface GroupDetails {
maxParticipants: number; maxParticipants: number;
portalCharge: number | null; portalCharge: number | null;
latePaymentCharge: number | null; latePaymentCharge: number | null;
lateFeeMode: 'DAILY' | 'FIXED' | null;
lateFeeValue: number | null;
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
customCycleDays: number; customCycleDays: number;
withdrawalMethod: 'RANDOM' | 'SEQUENTIAL'; withdrawalMethod: 'RANDOM' | 'SEQUENTIAL';
@@ -54,16 +62,23 @@ export interface EditGroupPayload {
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
latePaymentCharge: number; latePaymentCharge: number;
visibility: 'PUBLIC' | 'PRIVATE'; visibility: 'PUBLIC' | 'PRIVATE';
lateFeeMode: 'DAILY' | 'FIXED' | null;
lateFeeValue: number | null;
} }
type EditGroupScreenProps = NativeStackScreenProps<RootStackParamList, 'EditGroup'>; // Correct props for a screen inside a nested Tab Navigator
type EditingField = keyof EditGroupPayload | null; type EditGroupScreenProps = CompositeScreenProps<
BottomTabScreenProps<ManagementTabParamList, 'EditGroup'>,
NativeStackScreenProps<RootStackParamList>
>;
type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null;
// --- Main Screen Component --- // --- Main Screen Component ---
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => { const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const { groupId } = route.params; const { groupId } = route.params;
const [editingField, setEditingField] = useState<EditingField>(null); const [editingCard, setEditingCard] = useState<EditingCard>(null);
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null); const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
const [editableData, setEditableData] = useState<EditGroupPayload>({ const [editableData, setEditableData] = useState<EditGroupPayload>({
groupName: '', groupName: '',
@@ -74,30 +89,41 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
cycleType: 'MONTHLY', cycleType: 'MONTHLY',
latePaymentCharge: 0, latePaymentCharge: 0,
visibility: 'PRIVATE', visibility: 'PRIVATE',
lateFeeMode: null,
lateFeeValue: null,
}); });
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [starting, setStarting] = useState(false); const [starting, setStarting] = useState(false);
const [inviting, setInviting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// ... (All your handler functions: initializeEditableData, fetchGroupDetails, handleCancel, handleSave, etc. remain the same)
const initializeEditableData = (data: GroupDetails) => {
setEditableData({
groupName: data.groupName,
description: data.description,
maxParticipants: data.maxParticipants,
contributionAmount: data.contributionAmount,
customCycleDays: data.customCycleDays,
cycleType: data.cycleType,
latePaymentCharge: data.latePaymentCharge ?? 0,
visibility: data.visibility,
lateFeeMode: data.lateFeeMode ?? null,
lateFeeValue: data.lateFeeValue ?? null,
});
};
const fetchGroupDetails = useCallback(async () => { const fetchGroupDetails = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const response = await apiCall(`https://api.godemo.in/FundCollect/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string }; const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
console.log("groupDetails",response)
if (response.status === 'success' && response.data) { if (response.status === 'success' && response.data) {
setGroupDetails(response.data); setGroupDetails(response.data);
setEditableData({ initializeEditableData(response.data);
groupName: response.data.groupName,
description: response.data.description,
maxParticipants: response.data.maxParticipants,
contributionAmount: response.data.contributionAmount,
customCycleDays: response.data.customCycleDays,
cycleType: response.data.cycleType,
latePaymentCharge: response.data.latePaymentCharge ?? 0,
visibility: response.data.visibility,
});
} else { } else {
throw new Error(response.message || 'Failed to fetch group data.'); throw new Error(response.message || 'Failed to fetch group data.');
} }
@@ -114,36 +140,24 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const handleCancel = () => { const handleCancel = () => {
if (groupDetails) { if (groupDetails) {
setEditableData({ initializeEditableData(groupDetails);
groupName: groupDetails.groupName,
description: groupDetails.description,
maxParticipants: groupDetails.maxParticipants,
contributionAmount: groupDetails.contributionAmount,
customCycleDays: groupDetails.customCycleDays,
cycleType: groupDetails.cycleType,
latePaymentCharge: groupDetails.latePaymentCharge ?? 0,
visibility: groupDetails.visibility,
});
} }
setEditingField(null); setEditingCard(null);
}; };
const handleSave = async () => { const handleSave = async () => {
if (!groupDetails || !editingField) return; if (!groupDetails) return;
if (editingField === 'maxParticipants') { if (editableData.maxParticipants < groupDetails.totalParticipants) {
const newMaxParticipants = Number(editableData.maxParticipants);
if (newMaxParticipants < groupDetails.totalParticipants) {
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`); Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
return; return;
} }
}
setSaving(true); setSaving(true);
try { try {
await apiCall(`https://api.godemo.in/FundCollect/api/fund/groups/${groupDetails.id}/edit`, 'PUT', editableData); await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/groups/${groupDetails.id}/edit`, 'PUT', editableData);
Alert.alert('Success', 'Group has been updated.'); Alert.alert('Success', 'Group has been updated.');
setEditingField(null); setEditingCard(null);
await fetchGroupDetails(); await fetchGroupDetails();
} catch (e: any) { } catch (e: any) {
Alert.alert('Save Error', e.message || 'An unexpected error occurred.'); Alert.alert('Save Error', e.message || 'An unexpected error occurred.');
@@ -165,7 +179,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
onPress: async () => { onPress: async () => {
setStarting(true); setStarting(true);
try { try {
await apiCall(`https://api.godemo.in/FundCollect/api/fund/group/start?groupId=${groupDetails.id}`, 'GET'); await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/start?groupId=${groupDetails.id}`, 'GET');
Alert.alert('Success', 'Group has been started successfully.'); Alert.alert('Success', 'Group has been started successfully.');
await fetchGroupDetails(); await fetchGroupDetails();
} catch (e: any) { } catch (e: any) {
@@ -180,62 +194,104 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
); );
}; };
const handleInvite = async () => {
if (!groupDetails) return;
setInviting(true);
try {
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
const inviteLink = response.data;
if (inviteLink) {
Clipboard.setString(inviteLink);
Alert.alert('Copied!', 'Invitation link copied to clipboard.');
await Share.share({
message: `Join my group on CircleCollect! ${inviteLink}`,
url: inviteLink,
title: 'CircleCollect Group Invitation'
});
} else {
throw new Error('Could not generate invitation link.');
}
} catch (error: any) {
Alert.alert('Error', error.message || 'Failed to generate invitation.');
} finally {
setInviting(false);
}
};
const handleUpdateField = (field: keyof EditGroupPayload, value: any) => { const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
setEditableData(prev => ({ ...prev, [field]: value })); setEditableData(prev => ({ ...prev, [field]: value }));
}; };
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>; // ... (renderField and renderCard functions also remain the same)
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>; const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean) => {
if (!groupDetails) return null; const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
const renderInputField = (field: EditingField) => {
if (!field || !editableData) return null;
const commonProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
const getInputForField = () => {
switch(field) { switch(field) {
case 'groupName': case 'groupName':
return <TextInput {...commonProps} value={editableData.groupName} onChangeText={(text) => handleUpdateField('groupName', text)} />; return <TextInput {...commonInputProps} value={editableData.groupName} onChangeText={(text) => handleUpdateField('groupName', text)} />;
case 'description': case 'description':
return <TextInput {...commonProps} style={[styles.input, styles.textArea]} value={editableData.description} onChangeText={(text) => handleUpdateField('description', text)} multiline />; return <TextInput {...commonInputProps} style={[styles.input, styles.textArea]} value={editableData.description} onChangeText={(text) => handleUpdateField('description', text)} multiline />;
case 'contributionAmount': case 'contributionAmount':
return <TextInput {...commonInputProps} value={String(editableData.contributionAmount ?? '')} onChangeText={(text) => handleUpdateField('contributionAmount', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
case 'maxParticipants': case 'maxParticipants':
return <TextInput {...commonInputProps} value={String(editableData.maxParticipants ?? '')} onChangeText={(text) => handleUpdateField('maxParticipants', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
case 'latePaymentCharge': case 'latePaymentCharge':
case 'customCycleDays': return <TextInput {...commonInputProps} value={String(editableData.latePaymentCharge ?? '')} onChangeText={(text) => handleUpdateField('latePaymentCharge', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
return <TextInput {...commonProps} value={String(editableData[field] ?? '')} onChangeText={(text) => handleUpdateField(field, text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
case 'cycleType': case 'cycleType':
return <Picker selectedValue={editableData.cycleType} onValueChange={(itemValue) => handleUpdateField('cycleType', itemValue)} style={styles.picker}> return (
<Picker selectedValue={editableData.cycleType} onValueChange={(itemValue) => handleUpdateField('cycleType', itemValue)} style={styles.picker}>
{['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => <Picker.Item key={c} label={c} value={c} />)} {['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => <Picker.Item key={c} label={c} value={c} />)}
</Picker>; </Picker>
);
case 'customCycleDays':
if (editableData.cycleType !== 'CUSTOM') return null;
return <TextInput {...commonInputProps} value={String(editableData.customCycleDays ?? '')} onChangeText={(text) => handleUpdateField('customCycleDays', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
case 'visibility': case 'visibility':
return <Picker selectedValue={editableData.visibility} onValueChange={(itemValue) => handleUpdateField('visibility', itemValue)} style={styles.picker}> return (
<Picker selectedValue={editableData.visibility} onValueChange={(itemValue) => handleUpdateField('visibility', itemValue)} style={styles.picker}>
<Picker.Item label="PUBLIC" value="PUBLIC" /> <Picker.Item label="PUBLIC" value="PUBLIC" />
<Picker.Item label="PRIVATE" value="PRIVATE" /> <Picker.Item label="PRIVATE" value="PRIVATE" />
</Picker>; </Picker>
default: return null; );
} default:
return null;
} }
};
const renderRow = (field: EditingField, label: string, displayValue: string | number) => {
return ( return (
<View style={styles.fieldContainer}> <View style={styles.fieldContainer}>
<View style={styles.infoRow}>
<Text style={styles.infoLabel}>{label}</Text> <Text style={styles.infoLabel}>{label}</Text>
{editingField !== field && ( {isEditing && isEditable ? (
<View style={styles.valueContainer}> <View style={{marginTop: 8}}>{getInputForField()}</View>
) : (
<Text style={styles.infoValue}>{displayValue}</Text> <Text style={styles.infoValue}>{displayValue}</Text>
{isEditable && <TouchableOpacity onPress={() => setEditingField(field)} style={styles.editButton}>
<Text style={styles.editButtonText}>✎</Text>
</TouchableOpacity>}
</View>
)} )}
</View> </View>
{editingField === field && isEditable && ( );
<View style={styles.editingContainer}> }
{renderInputField(field)}
const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => {
const isEditingThisCard = editingCard === cardKey;
return (
<View style={styles.card}>
<View style={styles.cardHeader}>
<Text style={styles.cardTitle}>{title}</Text>
{isEditable && !editingCard && (
<TouchableOpacity onPress={() => setEditingCard(cardKey)} style={styles.editButton}>
<Text style={styles.editButtonText}>✎</Text>
</TouchableOpacity>
)}
</View>
{fields.map(({ field, label, displayValue }) =>
renderField(field, label, displayValue(), isEditingThisCard)
)}
{isEditingThisCard && (
<View style={styles.cardActions}> <View style={styles.cardActions}>
<TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={handleCancel} disabled={saving}> <TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={handleCancel} disabled={saving}>
<Text style={[styles.buttonText, { color: '#374151' }]}>Cancel</Text> <Text style={[styles.buttonText, { color: '#374151' }]}>Cancel</Text>
@@ -244,47 +300,54 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
{saving ? <ActivityIndicator color="#FFFFFF" size="small" /> : <Text style={styles.buttonText}>Save</Text>} {saving ? <ActivityIndicator color="#FFFFFF" size="small" /> : <Text style={styles.buttonText}>Save</Text>}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</View>
)} )}
</View> </View>
) );
} }
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>;
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
if (!groupDetails) return null;
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
const isInvitable = groupDetails.status === 'APPROVED';
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
return ( return (
<AppView paddingHorizontal={0}> // Use AppView as the root container with flex: 1
<ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}> <AppView style={{ flex: 1, paddingHorizontal: 0 }}>
<Text style={styles.headerTitle}>Edit Group</Text> <View style={styles.headerContainer}>
<Text style={styles.headerTitle}>Manage Group</Text>
{canStartGroup && ( {isInvitable && (
<View style={styles.card}> <TouchableOpacity style={[styles.button, styles.inviteButton]} onPress={handleInvite} disabled={inviting}>
<Text style={styles.cardTitle}>Ready to Start?</Text> {inviting ? <ActivityIndicator color="#FFFFFF" /> : <Text style={styles.buttonText}>Invite</Text>}
<Text style={styles.startGroupText}>This group has met the minimum participant requirement.</Text>
<TouchableOpacity style={[styles.button, styles.startButton]} onPress={handleStartGroup} disabled={starting}>
{starting ? <ActivityIndicator color="#FFFFFF" /> : <Text style={styles.buttonText}>Start Group</Text>}
</TouchableOpacity> </TouchableOpacity>
</View>
)} )}
<View style={styles.card}>
<Text style={styles.cardTitle}>Group Info</Text>
{renderRow('groupName', 'Group Name', groupDetails.groupName)}
{renderRow('description', 'Description', groupDetails.description)}
</View> </View>
<View style={styles.card}> <ScrollView
<Text style={styles.cardTitle}>Financial Rules</Text> style={styles.container}
{renderRow('contributionAmount', 'Contribution', `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`)} contentContainerStyle={styles.contentContainer}
{renderRow('maxParticipants', 'Max Participants', `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}`)} showsVerticalScrollIndicator={false}
{renderRow('latePaymentCharge', 'Late Fee', `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}`)} >
</View> {renderCard('Group Info', [
{ field: 'groupName', label: 'Group Name', displayValue: () => groupDetails.groupName },
{ field: 'description', label: 'Description', displayValue: () => groupDetails.description },
], 'groupInfo')}
<View style={styles.card}> {renderCard('Financial Rules', [
<Text style={styles.cardTitle}>Configuration</Text> { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{renderRow('cycleType', 'Payment Cycle', groupDetails.cycleType)} { field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
{(editableData.cycleType === 'CUSTOM' || groupDetails.cycleType === 'CUSTOM') && renderRow('customCycleDays', 'Custom Days', `${editableData.customCycleDays ?? groupDetails.customCycleDays} days`)} { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
{renderRow('visibility', 'Visibility', groupDetails.visibility)} ], 'financialRules')}
</View>
{renderCard('Configuration', [
{ field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType },
{ field: 'customCycleDays', label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays ?? ''} days` },
{ field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility },
], 'configuration')}
{/* Participants Card ... */}
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text> <Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
{groupDetails.participants.length > 0 ? ( {groupDetails.participants.length > 0 ? (
@@ -304,48 +367,104 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
)} )}
</View> </View>
</ScrollView> </ScrollView>
{/* --- FLOATING BUTTON AREA --- */}
{/* This View is a sibling of ScrollView, allowing it to float on top */}
{canStartGroup && (
<View style={styles.floatingButtonContainer}>
<TouchableOpacity
style={styles.activateButton}
onPress={handleStartGroup}
disabled={starting}
>
{starting ? (
<ActivityIndicator color="#FFFFFF" size="small" />
) : (
<Text style={styles.activateButtonText}>Activate Group</Text>
)}
</TouchableOpacity>
</View>
)}
</AppView> </AppView>
); );
}; };
// --- Styles ---
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' }, container: { flex: 1, backgroundColor: '#F3F4F6' },
contentContainer: { padding: 16, paddingBottom: 40 }, // Added more paddingBottom to ensure content doesn't hide behind the button AND the tab bar
contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' }, errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
headerTitle: { fontSize: 28, fontWeight: 'bold', color: '#111827', marginBottom: 20 }, headerContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: 8,
backgroundColor: 'white', // Changed for a cleaner header look
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
headerTitle: { fontSize: 28, fontWeight: 'bold', color: '#111827' },
card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 }, card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 },
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937', marginBottom: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10 }, cardHeader: {
flexDirection: 'row',
editButton: { padding: 8, marginLeft: 8 }, justifyContent: 'space-between',
editButtonText: { color: '#4F46E5', fontSize: 20, fontWeight: 'bold' }, alignItems: 'center',
paddingBottom: 10,
fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingVertical: 12 }, marginBottom: 12,
infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, },
infoLabel: { fontSize: 16, color: '#6B7280' }, cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937' },
editButton: { padding: 8 },
editButtonText: { color: colors.pastelPink, fontSize: 20, fontWeight: 'bold' },
fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingBottom: 12, marginBottom: 12 },
infoLabel: { fontSize: 16, color: '#6B7280', marginBottom: 4 },
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' }, infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
valueContainer: { flexDirection: 'row', alignItems: 'center' },
editingContainer: { marginTop: 12 },
input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 14, fontSize: 16, color: '#111827' }, input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 14, fontSize: 16, color: '#111827' },
textArea: { height: 100, textAlignVertical: 'top' }, textArea: { height: 100, textAlignVertical: 'top' },
picker: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 0, margin:0 }, picker: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10 },
participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' }, participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' },
participantName: { fontSize: 16, fontWeight: '600', color: '#111827' }, participantName: { fontSize: 16, fontWeight: '600', color: '#111827' },
participantMobile: { fontSize: 14, color: '#6B7280', marginTop: 4 }, participantMobile: { fontSize: 14, color: '#6B7280', marginTop: 4 },
organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 }, organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 },
organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' }, organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' },
noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 20 }, noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 20 },
cardActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#E5E7EB' }, cardActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#E5E7EB' },
button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center', justifyContent: 'center' }, button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center', justifyContent: 'center' },
saveButton: { backgroundColor: '#4F46E5' }, saveButton: { backgroundColor: colors.pastelOrange },
cancelButton: { backgroundColor: '#E5E7EB' }, cancelButton: { backgroundColor: '#E5E7EB' },
startButton: { backgroundColor: '#22c55e', marginTop: 10 }, inviteButton: { backgroundColor: colors.primary },
startGroupText: { fontSize: 15, color: '#6B7280', lineHeight: 22, marginBottom: 10}, buttonText: { color: '#000000ff', fontSize: 15, fontWeight: '600' },
buttonText: { color: '#FFFFFF', fontSize: 15, fontWeight: '600' },
// --- FLOATING BUTTON STYLES ---
floatingButtonContainer: {
position: 'absolute',
bottom: 85, // Positioned above the tab bar
left: 20,
right: 20,
alignItems: 'center', // Center the button horizontally
zIndex: 1, // Ensure it's on top
},
activateButton: {
backgroundColor: colors.pastelOrange,
borderRadius: 14,
paddingVertical: 16,
paddingHorizontal: 32,
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 4,
width: '90%', // Adjust width as needed
alignItems: 'center',
},
activateButtonText: {
color: '#000000ff',
fontSize: 18,
fontWeight: 'bold',
},
}); });
export default EditGroupScreen; export default EditGroupScreen;
@@ -0,0 +1,18 @@
import { StyleSheet, Text, View } from 'react-native'
import React from 'react'
import { useRoute } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../../navigation/AppNavigator';
type ManageJoinedGroupProps = NativeStackScreenProps<RootStackParamList, 'ManageJoinedGroup'>;
export default function ManageJoinedGroup({ route }: ManageJoinedGroupProps) {
const { groupId } = route.params as { groupId: string | number };
console.log("first", groupId)
return (
<View>
<Text>Manage Group: {groupId}</Text>
</View>
)
}
const styles = StyleSheet.create({})
+44 -61
View File
@@ -14,103 +14,66 @@ import {
import { useNavigation, useFocusEffect } from "@react-navigation/native"; import { useNavigation, useFocusEffect } from "@react-navigation/native";
import { StackNavigationProp } from "@react-navigation/stack"; import { StackNavigationProp } from "@react-navigation/stack";
import { RootStackParamList } from "../../navigation/AppNavigator"; 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 } 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 * as Keychain from "react-native-keychain";
import { MyGroup, JoinedGroup, JoinRequest, ApiResponse } from '../../types'; 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 Ionicons from "@react-native-vector-icons/ionicons";
import { UserRequested } from "../../components/UserRequested";
type GroupManagementNavProp = StackNavigationProp<RootStackParamList>; type GroupManagementNavProp = StackNavigationProp<RootStackParamList>;
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 user = useSelector((state:any) => state.user) const userRole = useSelector((state: any) => state.user)
const [activeTab, setActiveTab] = useState<ActiveTab>("My Groups"); const [activeTab, setActiveTab] = useState<ActiveTab>(userRole.role === "USER" ? "Joined Groups" : "My Groups");
const navigation = useNavigation<GroupManagementNavProp>(); const navigation = useNavigation<GroupManagementNavProp>();
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 [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 [userRole,setUserRole] = useState<string | null>(null) const [userRequest, setUserRequest] = useState<UserJoinRequest[]>([])
const userStatus = async () => {
try {
const credentials = await Keychain.getGenericPassword();
if (!credentials) {
Alert.alert("Error", "You are not logged in");
return;
}
const token = credentials.password;
setLoading(true);
setError(null);
const response = await fetch(`${BASE_URL}user/profile/status`, {
method: "GET",
headers: {
Accept: "*/*",
Authorization: `Bearer ${token}`,
},
});
const json = await response.json();
console.log("👤 Profile Status Response:", json.data.role);
if (json.status === "success") {
console.log("User Role:", json.data);
setUserRole(json.data.role || user.role);
if (json.data.profileStatus === false) {
}
} else {
setError(json.message || "Failed to fetch status");
}
} catch (err) {
setError("Something went wrong. Please try again.");
} finally {
setLoading(false);
}
};
useFocusEffect(
useCallback(() => {
userStatus();
}, [])
);
const fetchData = useCallback(async () => { const fetchData = useCallback(async () => {
if (loading) return; if (loading) return;
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
console.log(" abin user: ", userRole) if (userRole.role === "ORGANIZER") {
console.log('----',user) // Fetch all data concurrently for organizers
if (userRole === "ORGANIZER") {
// Fetch all data concurrently
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[]>>, // Assuming this is also needed 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("My Groups:", myGroupsRes.data);
console.log("Joined Groups:", joinedGroupsRes.data); console.log("Joined Groups:", joinedGroupsRes.data);
console.log("Requests:", requestsRes.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 for non-organizers // Fetch only joined groups and user requests for non-organizers
const [joinedGroupsRes] = await Promise.all([ const [joinedGroupsRes, userRequestRes] = await Promise.all([
apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>, // Assuming this is also needed apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>,
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 || []);
} }
} catch (e) { } catch (e) {
console.error("Fetch Data Error:", e); console.error("Fetch Data Error:", e);
@@ -121,6 +84,10 @@ const ManagementScreen: FC = () => {
}, [userRole, loading]); }, [userRole, loading]);
useFocusEffect(useCallback(() => { fetchData(); }, [])); useFocusEffect(useCallback(() => { fetchData(); }, []));
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) => { const handleAction = async (url: string, body: object, successMessage: string, failureMessage: string, action: string) => {
// ... (logic remains the same) // ... (logic remains the same)
@@ -146,7 +113,7 @@ const ManagementScreen: FC = () => {
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", { 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 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 handleManageGroup = (group: MyGroup) => navigation.navigate('EditGroup', { groupId: group.id }); const handleManageGroup = (group: MyGroup) => navigation.navigate('ManagementTabs', { groupId: group.id });
const renderContent = () => { const renderContent = () => {
if (error) { if (error) {
@@ -176,7 +143,7 @@ const ManagementScreen: FC = () => {
return ( return (
<FlatList <FlatList
data={joinedGroups} data={joinedGroups}
renderItem={({ item }: ListRenderItemInfo<JoinedGroup>) => <JoinedGroupCard item={item} />} renderItem={({ item }: ListRenderItemInfo<JoinedGroup>) => <JoinedGroupCard item={item} onViewPress={() => handleViewGroup(item)} />}
keyExtractor={(item) => item.id} keyExtractor={(item) => item.id}
ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>} ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>}
onRefresh={fetchData} onRefresh={fetchData}
@@ -199,12 +166,28 @@ const ManagementScreen: FC = () => {
contentContainerStyle={{ paddingBottom: 100 }} contentContainerStyle={{ paddingBottom: 100 }}
/> />
); );
case "Your Requests":
if (loading && requests.length === 0) {
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 === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"]; const tabs: ActiveTab[] = userRole.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}>
@@ -0,0 +1,162 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { View, Text, StyleSheet, Platform, TouchableOpacity } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Ionicons from 'react-native-vector-icons/Ionicons'; // Correct import for react-native-vector-icons
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
import EditGroupScreen from './EditGroupScreen'; // Your screen component
import Payments from './Payments';
import Member from './Member';
type Props = NativeStackScreenProps<RootStackParamList, 'ManagementTabs'>;
const Tab = createBottomTabNavigator();
// --- Custom Floating Tab Bar Component ---
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) => {
const insets = useSafeAreaInsets();
return (
<View style={[styles.tabBarContainer, { bottom: insets.bottom > 0 ? insets.bottom : 16 }]}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const label = options.tabBarLabel ?? options.title ?? route.name;
// A custom option to pass the icon name
const iconName = options.tabBarIconName ?? 'help-circle';
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name, route.params);
}
};
const onLongPress = () => {
navigation.emit({
type: 'tabLongPress',
target: route.key,
});
};
return (
<TouchableOpacity
key={route.key}
accessibilityRole="button"
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={options.tabBarAccessibilityLabel}
testID={options.tabBarButtonTestID}
onPress={onPress}
onLongPress={onLongPress}
style={styles.tabItem}
>
<Ionicons
name={iconName}
size={24}
color={isFocused ? '#4F46E5' : '#6B7280'}
/>
<Text style={[styles.tabLabel, { color: isFocused ? '#4F46E5' : '#6B7280' }]}>
{label}
</Text>
</TouchableOpacity>
);
})}
</View>
);
};
// --- Main Tab Navigator Component ---
const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
const { groupId } = route.params
return (
<Tab.Navigator
// Assign the custom floating tab bar
tabBar={props => <FloatingTabBar {...props} />}
screenOptions={{
headerShown: false, // Hides the default header for all screens in the tab
}}
>
<Tab.Screen
name="EditGroup"
component={EditGroupScreen}
// Pass the groupId to the EditGroupScreen as an initial parameter
initialParams={{ groupId }}
options={{
tabBarLabel: 'Settings',
// Custom prop to define the icon for our FloatingTabBar
}}
/>
<Tab.Screen
name="Payments"
component={Payments}
// Pass the groupId to the EditGroupScreen as an initial parameter
initialParams={{ groupId }}
options={{
tabBarLabel: 'Payments',
}}
/>
<Tab.Screen
name="Member"
component={Member}
// Pass the groupId to the EditGroupScreen as an initial parameter
initialParams={{ groupId }}
options={{
tabBarLabel: 'Members',
}}
/>
{/* You can easily add more tabs here in the future */}
</Tab.Navigator>
);
};
// --- Styles ---
const styles = StyleSheet.create({
tabBarContainer: {
flexDirection: 'row',
position: 'absolute',
left: 30,
right: 30,
backgroundColor: 'white',
borderRadius: 60,
paddingVertical: 4,
alignItems: 'center',
justifyContent: 'space-around',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.1,
shadowRadius: 10,
elevation: 8,
},
tabItem: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 8,
},
tabLabel: {
fontSize: 12,
marginTop: 4,
fontWeight: '600',
},
});
export default ManagementTabNavigation;
+296
View File
@@ -0,0 +1,296 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
StyleSheet,
Text,
View,
FlatList,
ActivityIndicator,
SafeAreaView,
Image,
TouchableOpacity,
Alert,
} from 'react-native';
import { Ionicons } from "@react-native-vector-icons/ionicons"; // Assuming you use Expo or have vector icons installed
import { apiCall } from '../../utils/ApiHelper'; // Adjust path to your apiCall utility
import AppView from '../../components/AppView';
import colors from '../../components/colors';
// --- Type Definitions ---
interface Member {
id: string;
userId: number;
firstName: string;
lastName: string | null;
mobile: string;
gender: string;
email: string;
username: string;
address: string;
role: 'USER' | 'ORGANIZER';
}
interface ApiResponse {
message: string;
status: string;
statusCode: number;
data: Member[];
}
// --- Main Screen Component ---
// This assumes the component receives `groupId` from a navigator (e.g., Tab Navigator)
const MemberScreen = () => {
const route = use
const { groupId } = route.params;
const [members, setMembers] = useState<Member[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const fetchMembers = useCallback(async () => {
if (!groupId) {
setError('Group ID is missing.');
setLoading(false);
return;
}
setLoading(true);
try {
const response = await apiCall<ApiResponse>(
`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/all/member?groupId=${groupId}`,
'GET'
);
if (response && response.status === 'success') {
setMembers(response.data);
setError(null);
} else {
throw new Error(response?.message || 'Failed to fetch members.');
}
} catch (e: any) {
setError(e.message || 'An unexpected network error occurred.');
} finally {
setLoading(false);
}
}, [groupId]);
useEffect(() => {
fetchMembers();
}, [fetchMembers]);
// --- Render Functions ---
const renderItem = ({ item }: { item: Member }) => {
const fullName = `${item.firstName} ${item.lastName || ''}`.trim();
const initial = item.firstName ? item.firstName.charAt(0).toUpperCase() : '?';
const isOrganizer = item.role === 'ORGANIZER';
return (
<TouchableOpacity style={styles.card}>
<View style={styles.avatarPlaceholder}>
<Text style={styles.avatarText}>{initial}</Text>
</View>
<View style={styles.infoContainer}>
<View style={{flexDirection:"row",justifyContent:"space-between"}}>
<Text style={styles.name}>{fullName}</Text>
{isOrganizer && (
<View style={styles.organizerIcon}>
<Text>Orginzer</Text>
</View>
)}
</View>
<Text style={styles.username}>@{item.username}</Text>
</View>
</TouchableOpacity>
);
};
const renderContent = () => {
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" color="#4F46E5" />
<Text style={styles.loadingText}>Loading Members...</Text>
</View>
);
}
if (error) {
return (
<View style={styles.centered}>
<Ionicons name="cloud-offline-outline" size={60} color="#EF4444" />
<Text style={styles.errorTitle}>Oops!</Text>
<Text style={styles.errorText}>{error}</Text>
<TouchableOpacity style={styles.retryButton} onPress={fetchMembers}>
<Text style={styles.retryButtonText}>Try Again</Text>
</TouchableOpacity>
</View>
);
}
if (members.length === 0) {
return (
<View style={styles.centered}>
<Ionicons name="people-outline" size={60} color="#9CA3AF" />
<Text style={styles.emptyTitle}>No Members Yet</Text>
<Text style={styles.emptyText}>Invite people to join your group.</Text>
</View>
)
}
return (
<FlatList
data={members}
renderItem={renderItem}
keyExtractor={item => item.id}
contentContainerStyle={styles.listContainer}
showsVerticalScrollIndicator={false}
/>
);
};
return (
<AppView>
<View style={styles.header}>
<Text style={styles.headerTitle}>Group Members</Text>
<Text style={styles.headerSubtitle}>{members.length} member(s)</Text>
</View>
{renderContent()}
</AppView>
);
};
// --- Styles ---
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F3F4F6',
},
header: {
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 12,
backgroundColor: 'white',
borderBottomWidth: 1,
borderBottomColor: '#E5E7EB',
},
headerTitle: {
fontSize: 28,
fontWeight: 'bold',
color: '#111827',
},
headerSubtitle: {
fontSize: 16,
color: '#6B7280',
marginTop: 4,
},
listContainer: {
padding: 16,
},
card: {
flexDirection: 'row',
backgroundColor: '#FFFFFF',
padding: 16,
borderRadius: 12,
marginBottom: 16,
alignItems: 'center',
shadowColor: '#374151',
shadowOpacity: 0.08,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 3,
},
avatarPlaceholder: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: '#D1D5DB',
alignItems: 'center',
justifyContent: 'center',
marginRight: 16,
},
avatarText: {
color: '#4B5563',
fontSize: 24,
fontWeight: 'bold',
},
organizerIcon: {
borderRadius: 10,
padding: 2,
borderWidth: 2,
borderColor: colors.pastelOrange
},
infoContainer: {
flex: 1,
},
name: {
fontSize: 18,
fontWeight: '700',
color: '#111827',
},
username: {
fontSize: 14,
color: '#6B7280',
marginBottom: 8,
},
contactRow: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 4,
},
contactText: {
marginLeft: 8,
fontSize: 14,
color: '#374151',
},
menuButton: {
padding: 8,
marginLeft: 8,
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
loadingText: {
marginTop: 10,
fontSize: 16,
color: '#6B7280',
},
errorTitle: {
fontSize: 22,
fontWeight: 'bold',
color: '#EF4444',
marginBottom: 8,
},
errorText: {
fontSize: 16,
color: '#374151',
textAlign: 'center',
marginBottom: 20,
},
retryButton: {
backgroundColor: '#4F46E5',
paddingVertical: 12,
paddingHorizontal: 30,
borderRadius: 8,
},
retryButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
emptyTitle: {
fontSize: 22,
fontWeight: 'bold',
color: '#374151',
marginBottom: 8,
},
emptyText: {
fontSize: 16,
color: '#6B7280',
textAlign: 'center',
}
});
export default MemberScreen;
+12
View File
@@ -0,0 +1,12 @@
import { View, Text } from 'react-native'
import React from 'react'
const Payments = () => {
return (
<View>
<Text>Payments</Text>
</View>
)
}
export default Payments
+1 -1
View File
@@ -41,7 +41,7 @@ const BecomeOrg = () => {
const token = credentials.password; const token = credentials.password;
const response = await fetch( const response = await fetch(
"https://api.godemo.in/FundCollect/api/organizer/registration/apply", "https://exotic-humpback-mistakenly.ngrok-free.app/api/organizer/registration/apply",
{ {
method: "POST", method: "POST",
headers: { headers: {
-2
View File
@@ -327,11 +327,9 @@ const Profile = () => {
); );
}; };
console.log("statusData?.role :", statusData?.role); console.log("statusData?.role :", statusData?.role);
console.log("statusData?.organizerStatus :", statusData?.organizerStatus); console.log("statusData?.organizerStatus :", statusData?.organizerStatus);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
userStatus(); userStatus();
+11
View File
@@ -37,6 +37,17 @@ export interface JoinRequest {
userAvatar?: string; userAvatar?: string;
} }
export interface UserJoinRequest {
createdBy: string;
groupId: string;
groupName: string;
reason: string | null;
requestDate: string;
requestId: any;
status: string;
}
// src/types.ts // src/types.ts
+34 -10
View File
@@ -1,4 +1,4 @@
import axios, { AxiosRequestConfig } from "axios"; import axios, { AxiosRequestConfig, isAxiosError } from "axios";
import * as Keychain from "react-native-keychain"; import * as Keychain from "react-native-keychain";
/** /**
@@ -14,10 +14,19 @@ export async function apiCall<T>(
data?: any, data?: any,
config?: AxiosRequestConfig config?: AxiosRequestConfig
): Promise<T | null> { ): Promise<T | null> {
// --- Request Logging ---
console.log("--- API Request ---");
console.log(`[${method}] ${url}`);
if (data) {
console.log("Payload:", JSON.stringify(data, null, 2));
}
console.log("-------------------");
try { try {
// Get saved credentials from Keychain // Get saved credentials from Keychain
const creds = await Keychain.getGenericPassword(); const creds = await Keychain.getGenericPassword();
console.log("hihihi", creds); console.log("Keychain Credentials Found:", creds); // Log if creds were found
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
...(creds ? { Authorization: `Bearer ${creds.password}` } : {}), ...(creds ? { Authorization: `Bearer ${creds.password}` } : {}),
@@ -32,22 +41,37 @@ export async function apiCall<T>(
headers, headers,
}); });
// --- Response Logging ---
console.log(`--- API Response: [${response.status}] ${url} ---`);
// Uncomment the line below to see the full response data in logs
// console.log("Response Data:", JSON.stringify(response.data, null, 2));
console.log("---------------------------------------");
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
console.error("--- API Error ---");
console.error(`Request Failed: [${method}] ${url}`);
if (isAxiosError(error)) {
if (error.response) { if (error.response) {
// Server responded with a status other than 2xx // Server responded with a status other than 2xx
console.error( console.error(`Status: ${error.response.status}`);
"API Error:", console.error("Data:", JSON.stringify(error.response.data, null, 2));
`Status: ${error.response.status}`, console.error("Headers:", JSON.stringify(error.response.headers, null, 2));
`Data: ${JSON.stringify(error.response.data)}`
);
} else if (error.request) { } else if (error.request) {
// Request was made but no response received // Request was made but no response received
console.error("API Error: No response received", error.request); console.error("No response received. This could be a network issue or CORS problem.");
} else { } else {
// Something else went wrong while setting up the request // Something else went wrong while setting up the request
console.error("API Error:", error.message); console.error("Error setting up request:", error.message);
} }
return null; // return null so the caller can handle it } else {
// Non-Axios error
console.error("An unexpected error occurred:", error.message);
}
console.error("-----------------");
// Re-throw the error so that the calling function can handle it if needed
throw error;
} }
} }
+2
View File
@@ -15,4 +15,6 @@ export const GET_JOINED_GROUP_URL = `${BASE_URL}fund/my/join/groups`
export const PUT_USER_PROFILE_IMG = `${BASE_URL}user/update/userImg` export const PUT_USER_PROFILE_IMG = `${BASE_URL}user/update/userImg`
export const PUT_USER_PROFILE = `${BASE_URL}user/profile/update` export const PUT_USER_PROFILE = `${BASE_URL}user/profile/update`
export const PUT_USER_STATUS = `${BASE_URL}user/profile/status` 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 UPDATE_BANK_URL = `${BASE_URL}user/bank/update`;
+19
View File
@@ -59,3 +59,22 @@ export const fetchUserProfile = async () => {
Alert.alert("Error", "Could not connect to server"); Alert.alert("Error", "Could not connect to server");
} }
}; };
// src/utils/dateFormatter.ts
export const formatDate = (dateString: string): string => {
const date = new Date(dateString);
if (isNaN(date.getTime())) return "Invalid Date";
const options: Intl.DateTimeFormatOptions = {
day: "2-digit",
month: "short",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: true,
};
return new Intl.DateTimeFormat("en-GB", options).format(date);
};