diff --git a/package-lock.json b/package-lock.json index 054e8ec..9d3165a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "CircleCollect", "version": "0.0.1", "dependencies": { + "@react-native-clipboard/clipboard": "^1.16.3", "@react-native-community/slider": "^5.0.1", "@react-native-picker/picker": "^2.11.2", "@react-native-vector-icons/ionicons": "^12.3.0", @@ -2739,6 +2740,29 @@ "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": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.0.0.tgz", @@ -4791,6 +4815,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -8078,6 +8103,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -8834,6 +8860,7 @@ "version": "2.3.1", "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==", + "devOptional": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -9753,6 +9780,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -14761,6 +14789,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, "license": "MIT" }, "node_modules/throat": { @@ -15219,6 +15248,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "devOptional": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" diff --git a/package.json b/package.json index d5e72be..09318c4 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "test": "jest" }, "dependencies": { + "@react-native-clipboard/clipboard": "^1.16.3", "@react-native-community/slider": "^5.0.1", "@react-native-picker/picker": "^2.11.2", "@react-native-vector-icons/ionicons": "^12.3.0", diff --git a/src/components/JoinedGroupCard.tsx b/src/components/JoinedGroupCard.tsx index e2f9a52..6ef1d79 100644 --- a/src/components/JoinedGroupCard.tsx +++ b/src/components/JoinedGroupCard.tsx @@ -1,7 +1,8 @@ // src/components/JoinedGroupCard.tsx import React, { FC } from 'react'; -import { View, Text, StyleSheet } from 'react-native'; +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import colors from './colors'; // Update the type to match the full data structure interface JoinedGroup { @@ -23,6 +24,7 @@ interface JoinedGroup { interface JoinedGroupCardProps { item: JoinedGroup | any; + onViewPress: () => void; // Function prop for the View button } // Helper function to format dates @@ -58,7 +60,7 @@ const getStatusStyle = (status: JoinedGroup['status']) => { } }; -export const JoinedGroupCard: FC = ({ item }) => { +export const JoinedGroupCard: FC = ({ item, onViewPress }) => { const progress = (item.currentParticipants / item.maxParticipants) * 100; const statusStyle = getStatusStyle(item.status); @@ -96,11 +98,14 @@ export const JoinedGroupCard: FC = ({ item }) => { - {/* Dates */} + {/* Footer with Dates and View Button */} {formatDate(item.startDate)} - {formatDate(item.endDate)} + + View + ); @@ -151,7 +156,7 @@ const styles = StyleSheet.create({ paddingVertical: 16, borderTopWidth: 1, borderBottomWidth: 1, - borderColor: '#F3F4F6', + borderColor: colors.pastelOrange, marginBottom: 16, }, infoItem: { @@ -193,16 +198,33 @@ const styles = StyleSheet.create({ }, progressBarFill: { height: '100%', - backgroundColor: '#4F46E5', + backgroundColor: colors.Settings, borderRadius: 4, }, footer: { + flexDirection: 'row', + justifyContent: 'space-between', alignItems: 'center', marginTop: 8, + borderTopWidth: 1, + borderTopColor: '#F3F4F6', + paddingTop: 16, }, dateText: { fontSize: 14, color: '#4B5563', 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', + }, }); diff --git a/src/components/MyGroupCard.tsx b/src/components/MyGroupCard.tsx index c72c3e2..3521c8b 100644 --- a/src/components/MyGroupCard.tsx +++ b/src/components/MyGroupCard.tsx @@ -50,7 +50,7 @@ export const MyGroupCard: React.FC = ({ item, onManage }) => { {item.groupName} - {item.status === "ACTIVE" ? "Active" : "Not Active"} + {item.status} diff --git a/src/components/RequestCard.tsx b/src/components/RequestCard.tsx index b0ac2fd..ff8a7a2 100644 --- a/src/components/RequestCard.tsx +++ b/src/components/RequestCard.tsx @@ -50,16 +50,7 @@ export const RequestCard: FC = ({ 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({ card: { diff --git a/src/components/UserRequested.tsx b/src/components/UserRequested.tsx new file mode 100644 index 0000000..2b4999a --- /dev/null +++ b/src/components/UserRequested.tsx @@ -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 = ({ item }) => { + const { backgroundColor, textColor, label, icon } = getStatusStyle(item.status); + + return ( + + {/* Top row: User info + Status */} + + + + {item.groupName} + + + {" "} + {formatDate(item.requestDate)} + + + + {/* Status Badge */} + + + {label} + + + + {/* Group Info */} + + + Group Created By :{" "} + + {item.createdBy} + + + {item.reason ? ( + + Reason: {item.reason} + + ) : null} + + + ); +}; + +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, + }, +}); diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index b2c63a8..87c9b6d 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -1,18 +1,24 @@ import React from "react"; import { View, Text, TouchableOpacity, StyleSheet } from "react-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 = { title: string; showBack?: boolean; // optional, default true onBackPress?: () => void; // optional custom back action + rightAction?: React.ReactNode; // allow passing JSX for right side + titleColor?:string; + arrowColor?:string }; const Header: React.FC = ({ title, showBack = true, onBackPress, + rightAction, + titleColor = 'pink', + arrowColor='pink' }) => { const navigation = useNavigation(); @@ -26,12 +32,15 @@ const Header: React.FC = ({ return ( - {showBack && ( - - + {showBack ? ( + + + ) : ( + // placeholder to keep spacing )} - {title} + {title} + {rightAction} ); }; @@ -42,21 +51,11 @@ const styles = StyleSheet.create({ header: { flexDirection: "row", alignItems: "center", - paddingHorizontal: 16, + justifyContent: "space-between", paddingVertical: 12, - backgroundColor: "#fff", - shadowColor: "#000", - shadowOpacity: 0.05, - shadowOffset: { width: 0, height: 2 }, - shadowRadius: 6, - elevation: 3, - }, - backButton: { - marginRight: 12, }, headerTitle: { fontSize: 18, fontWeight: "600", - color: "#000", }, }); diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 3de28d5..03c35ad 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -15,6 +15,9 @@ import Transaction from "../screens/profile/Transaction"; import GroupDetails from "../screens/home/user-home/GroupDetails"; import EditGroupScreen from "../screens/management/EditGroupScreen"; 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 = { Splash: undefined; @@ -32,22 +35,39 @@ export type RootStackParamList = { YourContacts: undefined; Transaction: undefined; EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null }; + ManageJoinedGroup: { groupId: string | number }; + Invite: { groupId: string | number; code: string | number }; + ManagementTabs:any }; const Stack = createNativeStackNavigator(); export default function AppNavigator() { - const linking = { - prefixes: [ - "myapp://", // A custom scheme for internal or test links. - "https://192.168.1.19" // Your production domain for verified App Links. +const linking = { + prefixes: [ + "myapp://", // Your custom scheme + "https://your-production-domain.com" // Your website for Universal Links/App Links ], - config: { - screens: { - GroupDetails: "invite/:inviteCode", + config: { + screens: { + // 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 ( @@ -66,6 +86,9 @@ export default function AppNavigator() { + + + ); diff --git a/src/screens/GroupCreation.tsx b/src/screens/GroupCreation.tsx index 227fb68..050ae64 100644 --- a/src/screens/GroupCreation.tsx +++ b/src/screens/GroupCreation.tsx @@ -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 lateFeeModes = ["DAILY", "FIXED"]; + const lateFeeModes = ["DAILY", "MONTHLY"]; const validateForm = () => { const newErrors: any = {}; diff --git a/src/screens/GroupScreen.tsx b/src/screens/GroupScreen.tsx index f3a8b07..34036bc 100644 --- a/src/screens/GroupScreen.tsx +++ b/src/screens/GroupScreen.tsx @@ -21,7 +21,7 @@ import { StackNavigationProp } from "@react-navigation/stack"; import { RootStackParamList } from "../navigation/AppNavigator"; import AppView from "../components/AppView"; 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 { ApiResponse } from "./home/UserProfileStatus"; 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[]) => { if (!searchQuery.trim()) return data; @@ -126,22 +150,23 @@ export default function GroupScreen() { const handleJoinGroup = async (groupId: string, userId: string) => { console.log("Joining group:", groupId, "User:", userId); 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); - const res = await apiCall(url, "POST") as { statusCode?: number; message?: string }; + const res = await apiCall(url, "POST"); console.log("JOIN_GROUP_URL response:", res); - if (res.statusCode === 200) { - Alert.alert("Success", "You requested to join the group."); - } else { - Alert.alert("Error", res.message || "Failed to join group."); - } + // if (res.statusCode === 200) { + // Alert.alert("Success", "You requested to join the group."); + // } else { + // Alert.alert("Error", res.message || "Failed to join group."); + // } } catch (error) { console.error("Join Group Error:", error); Alert.alert("Error", "An unexpected error occurred."); } }; + const userStatus = async () => { try { const credentials = await Keychain.getGenericPassword(); @@ -239,7 +264,7 @@ export default function GroupScreen() { refreshControl={ } - ListFooterComponent={} + // ListFooterComponent={} /> ); }; @@ -265,7 +290,7 @@ export default function GroupScreen() { keyExtractor={(item) => item.id} renderItem={({ item }) => } style={styles.featuredList} - ListEmptyComponent={!loading && featuredGroups.length === 0 ? No featured groups. : null} + // ListEmptyComponent={!loading && featuredGroups.length === 0 ? No featured groups. : null} /> diff --git a/src/screens/InviteScreen.tsx b/src/screens/InviteScreen.tsx new file mode 100644 index 0000000..01e6303 --- /dev/null +++ b/src/screens/InviteScreen.tsx @@ -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; // Assuming 'Invite' is in your stack + +// --- Main Screen Component --- +const InviteScreen: FC = ({ route }) => { + const { groupId,code} = route.params; + + const [groupDetails, setGroupDetails] = useState(null); + const [loading, setLoading] = useState(true); + const [joining, setJoining] = useState(false); + const [error, setError] = useState(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 ; + if (error) return {error}; + if (!groupDetails) return null; + + const organizerName = `${groupDetails.organizerFirstName} ${groupDetails.organizerLastName || ''}`.trim(); + + return ( + + + You're Invited! + Join the group {groupDetails.groupName} + + {/* Group Info Card */} + + Group Details + + + + + + + {/* Participants Card */} + + Participants ({groupDetails.totalParticipants} / {groupDetails.maxParticipants}) + {groupDetails.participants.length > 0 ? ( + groupDetails.participants.map(p => ( + + {`${p.firstName} ${p.lastName || ''}`.trim()} + {p.role === 'ORGANIZER' && Organizer} + + )) + ) : ( + Be the first to join! + )} + + + + {/* Floating Join Button */} + + + {joining ? ( + + ) : ( + + {groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED' ? 'Join Group' : `Group is ${groupDetails.status}`} + + )} + + + + ); +}; + +// Helper component for displaying rows +const InfoRow = ({ label, value }: { label: string; value: string }) => ( + + {label} + {value} + +); + +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; + diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx index e37fdeb..5b37f77 100644 --- a/src/screens/LoginScreen.tsx +++ b/src/screens/LoginScreen.tsx @@ -19,7 +19,6 @@ import type { StackNavigationProp } from "@react-navigation/stack"; import { RootStackParamList } from "../navigation/AppNavigator"; import { CustomButton, CustomInput } from "../components/common/Custom"; import colors from "../components/colors"; -import { useSelector } from "react-redux"; const theme = { primary: "#111111", @@ -42,8 +41,7 @@ export default function LoginScreen() { const fade = useRef(new Animated.Value(0)).current; const slide = useRef(new Animated.Value(24)).current; const ctaScale = useRef(new Animated.Value(1)).current; - const user = useSelector((state: any) => state.user) - console.log("current user ----", user) + useEffect(() => { Animated.parallel([ Animated.timing(fade, { toValue: 1, duration: 600, useNativeDriver: true }), @@ -55,8 +53,6 @@ export default function LoginScreen() { if (mobile.length === 10) { setLoading(true); try { - console.log("LOGIN_URL console:", LOGIN_URL, "mobile:", mobile); - const res = await axios.get(LOGIN_URL, { 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 ( >([]); const route = useRoute(); const { mobile } = route.params; - const navigation = useNavigation>(); + const navigation = useNavigation(); // animations const fade = useRef(new Animated.Value(0)).current; const slide = useRef(new Animated.Value(24)).current; @@ -80,26 +77,39 @@ export default function OtpVerificationScreen() { const onSubmit = async (otp: string, mobile: string) => { - try { - const payload = { mobile, otp }; - const res = await axios.post( - VARIFY_OTP_URL, - payload, - { headers: { "Content-Type": "application/json", Accept: "*/*" } } - ); - - console.log("✅ OTP Verified:", res.data); - if (res.data.statusCode === 200) { - saveToken(res.data.data.jwt); - fetchUserProfile() - navigation.navigate('Home') - ToastAndroid.show("OTP Verified Successfully!", ToastAndroid.SHORT); + try { + const payload = { mobile, otp }; + const res = await axios.post( + VARIFY_OTP_URL, + payload, + { + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, } - } catch (err: any) { - console.error("❌ OTP Verification Error:", err?.response?.data || err.message); - ToastAndroid.show(err.message || "Invalid OTP. Please try again.", ToastAndroid.SHORT); + ); + + console.log("✅ OTP Verified:", res.data); + + if (res.data?.statusCode === 200) { + const { jwt, role } = res.data?.data || {}; + + saveToken(jwt); + + navigation.navigate("Home"); + + ToastAndroid.show("OTP Verified Successfully!", ToastAndroid.SHORT); } - }; + } catch (err: any) { + console.error("❌ OTP Verification Error:", err?.response?.data || err.message); + ToastAndroid.show( + err?.response?.data?.message || "Invalid OTP. Please try again.", + ToastAndroid.SHORT + ); + } +}; + const resendOtp = async () => { try { diff --git a/src/screens/SplashScreen.tsx b/src/screens/SplashScreen.tsx index ded22d9..8d9f920 100644 --- a/src/screens/SplashScreen.tsx +++ b/src/screens/SplashScreen.tsx @@ -12,6 +12,8 @@ import { StackNavigationProp } from "@react-navigation/stack"; const SplashScreen = () => { const navigation = useNavigation>(); const dispatch = useDispatch(); + + const fadeAnim = useRef(new Animated.Value(1)).current; @@ -101,7 +103,7 @@ const SplashScreen = () => { }; startSplash(); - }, [navigation, fadeAnim, dispatch]); + }, [navigation, fadeAnim, dispatch, ]); return ( diff --git a/src/screens/home/Home.tsx b/src/screens/home/Home.tsx index 9abb27d..551c946 100644 --- a/src/screens/home/Home.tsx +++ b/src/screens/home/Home.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { StyleSheet, Text, @@ -7,33 +7,108 @@ import { UIManager, Platform, Image, + Alert, } from "react-native"; import AppView from "../../components/AppView"; import UserProfileStatus from "./UserProfileStatus"; import colors from "../../components/colors"; import GroupsTabs from "./GroupsTabs"; -import { useSelector } from "react-redux"; -import { useNavigation } from "@react-navigation/native"; +import { useDispatch, useSelector } from "react-redux"; +import { RouteProp, useFocusEffect, useNavigation, useRoute } from "@react-navigation/native"; 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 if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) { 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 user = useSelector((state: any) => state.user); const navigation = useNavigation(); - console.log("redux user data in Home:", user); + const dispatch = useDispatch() + + const [apiUser, setApiUser] = useState(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 ( {/* Header */} - Hello, {user.firstName} {user.lastName}👋 + Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}👋 navigation.navigate("Profile" as never)}> - + @@ -41,8 +116,9 @@ const Home = () => { {/* Status */} - {user?.role === "USER" ? : - } + {/* */} + {user?.role === "USER" || apiUser?.role === "USER" ? : + } ); }; @@ -60,6 +136,4 @@ const styles = StyleSheet.create({ marginBottom: 12, }, greeting: { fontSize: 22, fontWeight: "600", color: colors.Home }, - - }); \ No newline at end of file diff --git a/src/screens/home/RequestList.tsx b/src/screens/home/RequestList.tsx index 5ec2f47..6668595 100644 --- a/src/screens/home/RequestList.tsx +++ b/src/screens/home/RequestList.tsx @@ -13,6 +13,7 @@ import { import { Ionicons } from '@react-native-vector-icons/ionicons'; import colors from '../../components/colors'; import DeclineModal from './DeclineModal'; +import * as keychain from 'react-native-keychain' // Define TypeScript interfaces interface GroupRequest { @@ -40,22 +41,29 @@ const RequestList: React.FC = () => { const [error, setError] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [processingRequest, setProcessingRequest] = useState(null); - const [showDeclineModal, setShowDeclineModal] = useState<{modal: boolean, requestId: string | null}>({modal: false, requestId: null}); + const [showDeclineModal, setShowDeclineModal] = useState<{ modal: boolean, requestId: string | null }>({ modal: false, requestId: null }); const [selectedRequestId, setSelectedRequestId] = useState(null); // Fetch data from API const fetchRequests = async (): Promise => { + + const credentials = await keychain.getGenericPassword(); + if (!credentials) { + Alert.alert("Error", "You are not logged in"); + return; + } + const token = credentials.password; try { setRefreshing(true); setError(null); 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", headers: { accept: "*/*", - Authorization: "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MjY1MjI5MzcxIiwidXNlcklkIjoiMTAwMDAxIiwiaWF0IjoxNzU4MDkxMjEzLCJleHAiOjE3NTgxNzc2MTN9.T3BvU7nl8fnjwwVobIWRmSFFxQzOjtyIct1Ba0i9lIU" + Authorization: `Bearer ${token}` } } ); @@ -116,7 +124,7 @@ const RequestList: React.FC = () => { // Make API call to approve the request 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", headers: { @@ -156,13 +164,13 @@ const RequestList: React.FC = () => { }; const handleDeclineConfirm = async (requestedId: string | null, reason: string) => { - setShowDeclineModal({modal: false, requestId: null}); + setShowDeclineModal({ modal: false, requestId: null }); try { setProcessingRequest(requestedId); // Make API call to decline the request 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", headers: { @@ -197,7 +205,7 @@ const RequestList: React.FC = () => { }// call your API }; - + const onRefresh = (): void => { fetchRequests(); @@ -332,7 +340,7 @@ const RequestList: React.FC = () => { setShowDeclineModal({modal:true, requestId: request.requestId})} + onPress={() => setShowDeclineModal({ modal: true, requestId: request.requestId })} disabled={processingRequest === request.requestId} > {processingRequest === request.requestId ? ( @@ -361,7 +369,7 @@ const RequestList: React.FC = () => { {showDeclineModal.modal && ( setShowDeclineModal({modal: false, requestId: null})} + onClose={() => setShowDeclineModal({ modal: false, requestId: null })} onSubmit={(reason) => handleDeclineConfirm(showDeclineModal.requestId, reason)} /> )} diff --git a/src/screens/home/UserProfileStatus.tsx b/src/screens/home/UserProfileStatus.tsx index 3da10fd..8cc2f58 100644 --- a/src/screens/home/UserProfileStatus.tsx +++ b/src/screens/home/UserProfileStatus.tsx @@ -10,7 +10,7 @@ import { } from "react-native"; import * as Keychain from "react-native-keychain"; 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"; export type ApiResponse = { @@ -54,7 +54,7 @@ const UserProfileStatus: React.FC = () => { const json: ApiResponse = await response.json(); - console.log("👤 Profile Status Response:", json); + console.log("Profile Status :", json); if (json.status === "success") { setStatusData(json.data); diff --git a/src/screens/home/user-home/GroupDetails.tsx b/src/screens/home/user-home/GroupDetails.tsx index c25acf3..4082231 100644 --- a/src/screens/home/user-home/GroupDetails.tsx +++ b/src/screens/home/user-home/GroupDetails.tsx @@ -11,6 +11,7 @@ import { useRoute } from "@react-navigation/native"; import AppView from "../../../components/AppView"; import colors from "../../../components/colors"; import Ionicons from "@react-native-vector-icons/ionicons"; +import Header from "../../../components/common/Header"; const GroupDetails = () => { const route = useRoute(); @@ -88,7 +89,8 @@ const GroupDetails = () => { return ( {/* Group Title */} - {group.name} +
+ {/* {group.name} */} {group.description} {/* Progress Bar */} diff --git a/src/screens/home/user-home/UserHome.tsx b/src/screens/home/user-home/UserHome.tsx index c0acf8a..5c4ad2f 100644 --- a/src/screens/home/user-home/UserHome.tsx +++ b/src/screens/home/user-home/UserHome.tsx @@ -1,65 +1,108 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { StyleSheet, Text, View, FlatList, TouchableOpacity, + Alert, } from "react-native"; import { useNavigation } from "@react-navigation/native"; 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 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; +type NavigationProp = NativeStackNavigationProp< + RootStackParamList, + "GroupDetails" +>; type Group = { id: string; - name: string; - members: number; - collected: number; - goal: number; + groupName: string; + description: string; + currentParticipants: 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 navigation = useNavigation(); + const [groups, setGroups] = useState([]); + const [loading, setLoading] = useState(false) 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 progress = Math.min(item.collected / item.goal, 1) * 100; - return ( navigation.navigate("GroupDetails", { groupId: item.id })} > - {item.name} + {item.groupName} + {item.description} + - {item.members} members - - - {/* Progress Bar */} - - + + {item.currentParticipants}/{item.maxParticipants} members + - ₹{item.collected} / ₹{item.goal} + Contribution: ₹{item.contributionAmount} + Status: {item.status} ); }; @@ -67,14 +110,14 @@ const UserHome = () => { return ( {/* Stats */} - + {totalGroups} Groups Joined - ₹{totalCollected} - Total Contributed + ₹{totalContribution} + Total Contribution @@ -93,16 +136,13 @@ const UserHome = () => { export default UserHome; const styles = StyleSheet.create({ - - statsRow: { flexDirection: "row", - gap: 10 + gap: 10, }, statBox: { flex: 1, backgroundColor: "#fff", - // marginHorizontal: 6, padding: 16, borderRadius: 12, alignItems: "center", @@ -143,26 +183,27 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: "600", color: colors.Home, - marginBottom: 6, + marginBottom: 4, + }, + description: { + fontSize: 13, + color: "#475569", + marginBottom: 8, }, detail: { fontSize: 13, color: "#475569", marginBottom: 8, }, - progressBar: { - height: 6, - borderRadius: 3, - backgroundColor: "#e2e8f0", - marginBottom: 6, - overflow: "hidden", - }, - progressFill: { - backgroundColor: colors.primary, - height: "100%", - }, amount: { fontSize: 13, color: "#64748b", + marginTop: 4, + }, + status: { + fontSize: 12, + fontWeight: "500", + color: colors.primary, + marginTop: 2, }, }); diff --git a/src/screens/management/EditGroupScreen.tsx b/src/screens/management/EditGroupScreen.tsx index 1478ab9..abfaa0b 100644 --- a/src/screens/management/EditGroupScreen.tsx +++ b/src/screens/management/EditGroupScreen.tsx @@ -9,11 +9,17 @@ import { TouchableOpacity, Alert, ActivityIndicator, + Share, } from 'react-native'; import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path import { apiCall } from '../../utils/ApiHelper'; // Adjust path import AppView from '../../components/AppView'; 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 --- interface Participant { @@ -35,6 +41,8 @@ export interface GroupDetails { maxParticipants: number; portalCharge: number | null; latePaymentCharge: number | null; + lateFeeMode: 'DAILY' | 'FIXED' | null; + lateFeeValue: number | null; cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; customCycleDays: number; withdrawalMethod: 'RANDOM' | 'SEQUENTIAL'; @@ -54,16 +62,23 @@ export interface EditGroupPayload { cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM'; latePaymentCharge: number; visibility: 'PUBLIC' | 'PRIVATE'; + lateFeeMode: 'DAILY' | 'FIXED' | null; + lateFeeValue: number | null; } -type EditGroupScreenProps = NativeStackScreenProps; -type EditingField = keyof EditGroupPayload | null; +// Correct props for a screen inside a nested Tab Navigator +type EditGroupScreenProps = CompositeScreenProps< + BottomTabScreenProps, + NativeStackScreenProps +>; + +type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null; // --- Main Screen Component --- const EditGroupScreen: React.FC = ({ route }) => { const { groupId } = route.params; - const [editingField, setEditingField] = useState(null); + const [editingCard, setEditingCard] = useState(null); const [groupDetails, setGroupDetails] = useState(null); const [editableData, setEditableData] = useState({ groupName: '', @@ -74,30 +89,41 @@ const EditGroupScreen: React.FC = ({ route }) => { cycleType: 'MONTHLY', latePaymentCharge: 0, visibility: 'PRIVATE', + lateFeeMode: null, + lateFeeValue: null, }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [starting, setStarting] = useState(false); + const [inviting, setInviting] = useState(false); const [error, setError] = useState(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 () => { setLoading(true); setError(null); 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) { setGroupDetails(response.data); - setEditableData({ - 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, - }); + initializeEditableData(response.data); } else { throw new Error(response.message || 'Failed to fetch group data.'); } @@ -114,36 +140,24 @@ const EditGroupScreen: React.FC = ({ route }) => { const handleCancel = () => { if (groupDetails) { - setEditableData({ - 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, - }); + initializeEditableData(groupDetails); } - setEditingField(null); + setEditingCard(null); }; const handleSave = async () => { - if (!groupDetails || !editingField) return; + if (!groupDetails) return; - if (editingField === 'maxParticipants') { - const newMaxParticipants = Number(editableData.maxParticipants); - if (newMaxParticipants < groupDetails.totalParticipants) { - Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`); - return; - } + if (editableData.maxParticipants < groupDetails.totalParticipants) { + Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`); + return; } setSaving(true); 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.'); - setEditingField(null); + setEditingCard(null); await fetchGroupDetails(); } catch (e: any) { Alert.alert('Save Error', e.message || 'An unexpected error occurred.'); @@ -165,7 +179,7 @@ const EditGroupScreen: React.FC = ({ route }) => { onPress: async () => { setStarting(true); 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.'); await fetchGroupDetails(); } catch (e: any) { @@ -179,124 +193,173 @@ const EditGroupScreen: React.FC = ({ 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) => { setEditableData(prev => ({ ...prev, [field]: value })); }; + + // ... (renderField and renderCard functions also remain the same) + const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean) => { + const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" }; + const getInputForField = () => { + switch(field) { + case 'groupName': + return handleUpdateField('groupName', text)} />; + case 'description': + return handleUpdateField('description', text)} multiline />; + case 'contributionAmount': + return handleUpdateField('contributionAmount', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; + case 'maxParticipants': + return handleUpdateField('maxParticipants', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; + case 'latePaymentCharge': + return handleUpdateField('latePaymentCharge', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; + case 'cycleType': + return ( + handleUpdateField('cycleType', itemValue)} style={styles.picker}> + {['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => )} + + ); + case 'customCycleDays': + if (editableData.cycleType !== 'CUSTOM') return null; + return handleUpdateField('customCycleDays', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; + case 'visibility': + return ( + handleUpdateField('visibility', itemValue)} style={styles.picker}> + + + + ); + default: + return null; + } + }; + + return ( + + {label} + {isEditing && isEditable ? ( + {getInputForField()} + ) : ( + {displayValue} + )} + + ); + } + + const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => { + const isEditingThisCard = editingCard === cardKey; + + return ( + + + {title} + {isEditable && !editingCard && ( + setEditingCard(cardKey)} style={styles.editButton}> + ✎ + + )} + + + {fields.map(({ field, label, displayValue }) => + renderField(field, label, displayValue(), isEditingThisCard) + )} + + {isEditingThisCard && ( + + + Cancel + + + {saving ? : Save} + + + )} + + ); + } + if (loading) return ; if (error) return {error}; 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; - const renderInputField = (field: EditingField) => { - if (!field || !editableData) return null; - - const commonProps = { style: styles.input, placeholderTextColor: "#9CA3AF" }; - - switch(field) { - case 'groupName': - return handleUpdateField('groupName', text)} />; - case 'description': - return handleUpdateField('description', text)} multiline />; - case 'contributionAmount': - case 'maxParticipants': - case 'latePaymentCharge': - case 'customCycleDays': - return handleUpdateField(field, text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />; - case 'cycleType': - return handleUpdateField('cycleType', itemValue)} style={styles.picker}> - {['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => )} - ; - case 'visibility': - return handleUpdateField('visibility', itemValue)} style={styles.picker}> - - - ; - default: return null; - } - } - - const renderRow = (field: EditingField, label: string, displayValue: string | number) => { - return ( - - - {label} - {editingField !== field && ( - - {displayValue} - {isEditable && setEditingField(field)} style={styles.editButton}> - ✎ - } - - )} - - {editingField === field && isEditable && ( - - {renderInputField(field)} - - - Cancel - - - {saving ? : Save} - - - - )} - - ) - } - return ( - - - Edit Group - - {canStartGroup && ( - - Ready to Start? - This group has met the minimum participant requirement. - - {starting ? : Start Group} - - + // Use AppView as the root container with flex: 1 + + + Manage Group + {isInvitable && ( + + {inviting ? : Invite} + )} + - - Group Info - {renderRow('groupName', 'Group Name', groupDetails.groupName)} - {renderRow('description', 'Description', groupDetails.description)} - + + {renderCard('Group Info', [ + { field: 'groupName', label: 'Group Name', displayValue: () => groupDetails.groupName }, + { field: 'description', label: 'Description', displayValue: () => groupDetails.description }, + ], 'groupInfo')} - - Financial Rules - {renderRow('contributionAmount', 'Contribution', `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`)} - {renderRow('maxParticipants', 'Max Participants', `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}`)} - {renderRow('latePaymentCharge', 'Late Fee', `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}`)} - - - - Configuration - {renderRow('cycleType', 'Payment Cycle', groupDetails.cycleType)} - {(editableData.cycleType === 'CUSTOM' || groupDetails.cycleType === 'CUSTOM') && renderRow('customCycleDays', 'Custom Days', `${editableData.customCycleDays ?? groupDetails.customCycleDays} days`)} - {renderRow('visibility', 'Visibility', groupDetails.visibility)} - + {renderCard('Financial Rules', [ + { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` }, + { field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` }, + { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` }, + ], 'financialRules')} + {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 ... */} Participants ({groupDetails.totalParticipants}) {groupDetails.participants.length > 0 ? ( groupDetails.participants.map(p => ( - - - {`${p.firstName} ${p.lastName || ''}`.trim()} - {p.role === 'ORGANIZER' && Organizer} + + + {`${p.firstName} ${p.lastName || ''}`.trim()} + {p.role === 'ORGANIZER' && Organizer} + + {p.mobile} - {p.mobile} - )) ) : ( @@ -304,48 +367,104 @@ const EditGroupScreen: React.FC = ({ route }) => { )} + + {/* --- FLOATING BUTTON AREA --- */} + {/* This View is a sibling of ScrollView, allowing it to float on top */} + {canStartGroup && ( + + + {starting ? ( + + ) : ( + Activate Group + )} + + + )} ); }; +// --- Styles --- const styles = StyleSheet.create({ - container: { flex: 1, backgroundColor: '#F3F4F6' }, - contentContainer: { padding: 16, paddingBottom: 40 }, - centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, - errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' }, - headerTitle: { fontSize: 28, fontWeight: 'bold', color: '#111827', marginBottom: 20 }, - - 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 }, - - editButton: { padding: 8, marginLeft: 8 }, - editButtonText: { color: '#4F46E5', fontSize: 20, fontWeight: 'bold' }, - - fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingVertical: 12 }, - infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }, - infoLabel: { fontSize: 16, color: '#6B7280' }, - infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' }, - valueContainer: { flexDirection: 'row', alignItems: 'center' }, + container: { flex: 1, backgroundColor: '#F3F4F6' }, + // 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' }, + errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' }, + 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 }, + cardHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingBottom: 10, + marginBottom: 12, + }, + 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' }, + input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 14, fontSize: 16, color: '#111827' }, + textArea: { height: 100, textAlignVertical: 'top' }, + picker: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10 }, + participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' }, + participantName: { fontSize: 16, fontWeight: '600', color: '#111827' }, + participantMobile: { fontSize: 14, color: '#6B7280', marginTop: 4 }, + organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 }, + organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' }, + noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 20 }, + 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' }, + saveButton: { backgroundColor: colors.pastelOrange }, + cancelButton: { backgroundColor: '#E5E7EB' }, + inviteButton: { backgroundColor: colors.primary }, + buttonText: { color: '#000000ff', fontSize: 15, fontWeight: '600' }, - editingContainer: { marginTop: 12 }, - input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 14, fontSize: 16, color: '#111827' }, - textArea: { height: 100, textAlignVertical: 'top' }, - picker: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 0, margin:0 }, - - participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' }, - participantName: { fontSize: 16, fontWeight: '600', color: '#111827' }, - participantMobile: { fontSize: 14, color: '#6B7280', marginTop: 4 }, - organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 }, - organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' }, - noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 20 }, - - 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' }, - saveButton: { backgroundColor: '#4F46E5' }, - cancelButton: { backgroundColor: '#E5E7EB' }, - startButton: { backgroundColor: '#22c55e', marginTop: 10 }, - startGroupText: { fontSize: 15, color: '#6B7280', lineHeight: 22, marginBottom: 10}, - 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; diff --git a/src/screens/management/ManageJoinedGroup.tsx b/src/screens/management/ManageJoinedGroup.tsx new file mode 100644 index 0000000..0f9c7c0 --- /dev/null +++ b/src/screens/management/ManageJoinedGroup.tsx @@ -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; +export default function ManageJoinedGroup({ route }: ManageJoinedGroupProps) { + + const { groupId } = route.params as { groupId: string | number }; + console.log("first", groupId) + return ( + + Manage Group: {groupId} + + ) +} + +const styles = StyleSheet.create({}) \ No newline at end of file diff --git a/src/screens/management/ManagementScreen.tsx b/src/screens/management/ManagementScreen.tsx index 92f2b22..0eeaba0 100644 --- a/src/screens/management/ManagementScreen.tsx +++ b/src/screens/management/ManagementScreen.tsx @@ -14,103 +14,66 @@ import { import { useNavigation, useFocusEffect } from "@react-navigation/native"; import { StackNavigationProp } from "@react-navigation/stack"; import { RootStackParamList } from "../../navigation/AppNavigator"; -import { APPROVE_REQUEST_URL, BASE_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, JOIN_GROUP_URL, MY_GROUPS_URL, REQUESTS_URL } 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 AppView from "../../components/AppView"; import { apiCall } from "../../utils/ApiHelper"; 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 { JoinedGroupCard } from "../../components/JoinedGroupCard"; import { RequestCard } from "../../components/RequestCard"; import { useSelector } from "react-redux"; import Ionicons from "@react-native-vector-icons/ionicons"; +import { UserRequested } from "../../components/UserRequested"; type GroupManagementNavProp = StackNavigationProp; type ActiveTab = "My Groups" | "Joined Groups" | "Requests" | "Your Requests"; const ManagementScreen: FC = () => { - const user = useSelector((state:any) => state.user) - const [activeTab, setActiveTab] = useState("My Groups"); + const userRole = useSelector((state: any) => state.user) + const [activeTab, setActiveTab] = useState(userRole.role === "USER" ? "Joined Groups" : "My Groups"); const navigation = useNavigation(); const [myGroups, setMyGroups] = useState([]); const [joinedGroups, setJoinedGroups] = useState([]); const [requests, setRequests] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [userRole,setUserRole] = useState(null) - 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 [userRequest, setUserRequest] = useState([]) const fetchData = useCallback(async () => { if (loading) return; + setLoading(true); setError(null); + try { - console.log(" abin user: ", userRole) - console.log('----',user) - if (userRole === "ORGANIZER") { - // Fetch all data concurrently + if (userRole.role === "ORGANIZER") { + // Fetch all data concurrently for organizers const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([ apiCall(MY_GROUPS_URL, "GET") as Promise>, - apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>, // Assuming this is also needed + apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>, apiCall(REQUESTS_URL, "GET") as Promise>, ]); console.log("My Groups:", myGroupsRes.data); console.log("Joined Groups:", joinedGroupsRes.data); console.log("Requests:", requestsRes.data); + setMyGroups(myGroupsRes.data || []); setJoinedGroups(joinedGroupsRes.data || []); setRequests(requestsRes.data || []); } else { - // Fetch only joined groups for non-organizers - const [joinedGroupsRes] = await Promise.all([ - apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>, // Assuming this is also needed + // Fetch only joined groups and user requests for non-organizers + const [joinedGroupsRes, userRequestRes] = await Promise.all([ + apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>, + apiCall(USER_REQUEST, "GET") as Promise>, ]); - setJoinedGroups(joinedGroupsRes.data || []); + + console.log("Joined Groups:", joinedGroupsRes.data); + console.log("User Requests:", userRequestRes.data); + + setJoinedGroups(joinedGroupsRes.data || []); + setUserRequest(userRequestRes.data || []); } } catch (e) { console.error("Fetch Data Error:", e); @@ -121,6 +84,10 @@ const ManagementScreen: FC = () => { }, [userRole, loading]); 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) => { // ... (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 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 = () => { if (error) { @@ -176,7 +143,7 @@ const ManagementScreen: FC = () => { return ( ) => } + renderItem={({ item }: ListRenderItemInfo) => handleViewGroup(item)} />} keyExtractor={(item) => item.id} ListEmptyComponent={You haven't joined any groups yet.} onRefresh={fetchData} @@ -199,12 +166,28 @@ const ManagementScreen: FC = () => { contentContainerStyle={{ paddingBottom: 100 }} /> ); + case "Your Requests": + if (loading && requests.length === 0) { + return ; + } + return ( + ) => } + keyExtractor={(item) => item.requestId} + ListEmptyComponent={No pending join requests.} + onRefresh={fetchData} + refreshing={loading} // This handles the "continuing loading" on pull-to-refresh + contentContainerStyle={{ paddingBottom: 100 }} + /> + ); 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 ( diff --git a/src/screens/management/ManagementTabNavigation.tsx b/src/screens/management/ManagementTabNavigation.tsx new file mode 100644 index 0000000..239fbe3 --- /dev/null +++ b/src/screens/management/ManagementTabNavigation.tsx @@ -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; + +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 ( + 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 ( + + + + {label} + + + ); + })} + + ); +}; + +// --- Main Tab Navigator Component --- +const ManagementTabNavigation: React.FC = ({ route }) => { + const { groupId } = route.params + + return ( + } + screenOptions={{ + headerShown: false, // Hides the default header for all screens in the tab + }} + > + + + + {/* You can easily add more tabs here in the future */} + + ); +}; + +// --- 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; diff --git a/src/screens/management/Member.tsx b/src/screens/management/Member.tsx new file mode 100644 index 0000000..55c33ac --- /dev/null +++ b/src/screens/management/Member.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchMembers = useCallback(async () => { + if (!groupId) { + setError('Group ID is missing.'); + setLoading(false); + return; + } + + setLoading(true); + try { + const response = await apiCall( + `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 ( + + + + {initial} + + + + {fullName} + {isOrganizer && ( + + Orginzer + + )} + + @{item.username} + + + + ); + }; + + const renderContent = () => { + if (loading) { + return ( + + + Loading Members... + + ); + } + + if (error) { + return ( + + + Oops! + {error} + + Try Again + + + ); + } + + if (members.length === 0) { + return ( + + + No Members Yet + Invite people to join your group. + + ) + } + + return ( + item.id} + contentContainerStyle={styles.listContainer} + showsVerticalScrollIndicator={false} + /> + ); + }; + + return ( + + + Group Members + {members.length} member(s) + + {renderContent()} + + ); +}; + +// --- 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; diff --git a/src/screens/management/Payments.tsx b/src/screens/management/Payments.tsx new file mode 100644 index 0000000..7d261ff --- /dev/null +++ b/src/screens/management/Payments.tsx @@ -0,0 +1,12 @@ +import { View, Text } from 'react-native' +import React from 'react' + +const Payments = () => { + return ( + + Payments + + ) +} + +export default Payments \ No newline at end of file diff --git a/src/screens/profile/BecomeOrg.tsx b/src/screens/profile/BecomeOrg.tsx index bae00a7..0d63471 100644 --- a/src/screens/profile/BecomeOrg.tsx +++ b/src/screens/profile/BecomeOrg.tsx @@ -41,7 +41,7 @@ const BecomeOrg = () => { const token = credentials.password; 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", headers: { diff --git a/src/screens/profile/Profile.tsx b/src/screens/profile/Profile.tsx index 47bb309..85f980c 100644 --- a/src/screens/profile/Profile.tsx +++ b/src/screens/profile/Profile.tsx @@ -327,11 +327,9 @@ const Profile = () => { ); }; - console.log("statusData?.role :", statusData?.role); console.log("statusData?.organizerStatus :", statusData?.organizerStatus); - useFocusEffect( useCallback(() => { userStatus(); diff --git a/src/types.ts b/src/types.ts index 5ed5a76..fe88e58 100644 --- a/src/types.ts +++ b/src/types.ts @@ -37,6 +37,17 @@ export interface JoinRequest { userAvatar?: string; } +export interface UserJoinRequest { + createdBy: string; + groupId: string; + groupName: string; + reason: string | null; + requestDate: string; + requestId: any; + status: string; +} + + // src/types.ts diff --git a/src/utils/ApiHelper.tsx b/src/utils/ApiHelper.tsx index 84552fa..2bf9b5f 100644 --- a/src/utils/ApiHelper.tsx +++ b/src/utils/ApiHelper.tsx @@ -1,4 +1,4 @@ -import axios, { AxiosRequestConfig } from "axios"; +import axios, { AxiosRequestConfig, isAxiosError } from "axios"; import * as Keychain from "react-native-keychain"; /** @@ -14,10 +14,19 @@ export async function apiCall( data?: any, config?: AxiosRequestConfig ): Promise { + // --- Request Logging --- + console.log("--- API Request ---"); + console.log(`[${method}] ${url}`); + if (data) { + console.log("Payload:", JSON.stringify(data, null, 2)); + } + console.log("-------------------"); + try { // Get saved credentials from Keychain const creds = await Keychain.getGenericPassword(); - console.log("hihihi", creds); + console.log("Keychain Credentials Found:", creds); // Log if creds were found + const headers = { "Content-Type": "application/json", ...(creds ? { Authorization: `Bearer ${creds.password}` } : {}), @@ -32,22 +41,37 @@ export async function apiCall( 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; } catch (error: any) { - if (error.response) { - // Server responded with a status other than 2xx - console.error( - "API Error:", - `Status: ${error.response.status}`, - `Data: ${JSON.stringify(error.response.data)}` - ); - } else if (error.request) { - // Request was made but no response received - console.error("API Error: No response received", error.request); + console.error("--- API Error ---"); + console.error(`Request Failed: [${method}] ${url}`); + + if (isAxiosError(error)) { + if (error.response) { + // Server responded with a status other than 2xx + console.error(`Status: ${error.response.status}`); + console.error("Data:", JSON.stringify(error.response.data, null, 2)); + console.error("Headers:", JSON.stringify(error.response.headers, null, 2)); + } else if (error.request) { + // Request was made but no response received + console.error("No response received. This could be a network issue or CORS problem."); + } else { + // Something else went wrong while setting up the request + console.error("Error setting up request:", error.message); + } } else { - // Something else went wrong while setting up the request - console.error("API Error:", error.message); + // Non-Axios error + console.error("An unexpected error occurred:", error.message); } - return null; // return null so the caller can handle it + console.error("-----------------"); + + // Re-throw the error so that the calling function can handle it if needed + throw error; } } diff --git a/src/utils/ApiUrls.tsx b/src/utils/ApiUrls.tsx index 42e0c4b..7205ace 100644 --- a/src/utils/ApiUrls.tsx +++ b/src/utils/ApiUrls.tsx @@ -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 = `${BASE_URL}user/profile/update` 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`; diff --git a/src/utils/HelperFunction.tsx b/src/utils/HelperFunction.tsx index 1588b17..83c59b2 100644 --- a/src/utils/HelperFunction.tsx +++ b/src/utils/HelperFunction.tsx @@ -58,4 +58,23 @@ export const fetchUserProfile = async () => { console.error("❌ Error fetching profile:", error); Alert.alert("Error", "Could not connect to server"); } -}; \ No newline at end of file +}; + +// 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); +};