diff --git a/src/components/JoinedGroupCard.tsx b/src/components/JoinedGroupCard.tsx index e2f9a52..dcb9768 100644 --- a/src/components/JoinedGroupCard.tsx +++ b/src/components/JoinedGroupCard.tsx @@ -2,6 +2,7 @@ import React, { FC } from 'react'; import { View, Text, StyleSheet } from 'react-native'; +import colors from './colors'; // Update the type to match the full data structure interface JoinedGroup { @@ -151,7 +152,7 @@ const styles = StyleSheet.create({ paddingVertical: 16, borderTopWidth: 1, borderBottomWidth: 1, - borderColor: '#F3F4F6', + borderColor: colors.pastelOrange, marginBottom: 16, }, infoItem: { @@ -193,7 +194,7 @@ const styles = StyleSheet.create({ }, progressBarFill: { height: '100%', - backgroundColor: '#4F46E5', + backgroundColor: colors.Settings, borderRadius: 4, }, footer: { 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/screens/GroupScreen.tsx b/src/screens/GroupScreen.tsx index e46dd4d..34036bc 100644 --- a/src/screens/GroupScreen.tsx +++ b/src/screens/GroupScreen.tsx @@ -150,17 +150,17 @@ 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."); @@ -264,7 +264,7 @@ export default function GroupScreen() { refreshControl={ } - ListFooterComponent={} + // ListFooterComponent={} /> ); }; diff --git a/src/screens/OtpVarificationScreen.tsx b/src/screens/OtpVarificationScreen.tsx index 1b07871..0bd381c 100644 --- a/src/screens/OtpVarificationScreen.tsx +++ b/src/screens/OtpVarificationScreen.tsx @@ -77,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); - fetchUserProfile() - navigation.navigate("Home", { refreshProfile: true }); - 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/home/Home.tsx b/src/screens/home/Home.tsx index 231f8b7..551c946 100644 --- a/src/screens/home/Home.tsx +++ b/src/screens/home/Home.tsx @@ -7,21 +7,27 @@ 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 { useFocusEffect, useNavigation, useRoute } 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; @@ -33,9 +39,8 @@ type UserProfile = { const Home = () => { const user = useSelector((state: any) => state.user); const navigation = useNavigation(); - const route = useRoute(); + const dispatch = useDispatch() - console.log("route asf : ", route?.params) const [apiUser, setApiUser] = useState(null) const fetchUserProfile = async () => { @@ -60,7 +65,29 @@ const Home = () => { 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); @@ -68,10 +95,8 @@ const Home = () => { }; useEffect(() => { - if (route.params) { - fetchUserProfile(); - } - }, [route.params]); + fetchUserProfile(); + }, []); return ( @@ -91,6 +116,7 @@ const Home = () => { {/* Status */} + {/* */} {user?.role === "USER" || apiUser?.role === "USER" ? : } @@ -110,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/UserProfileStatus.tsx b/src/screens/home/UserProfileStatus.tsx index b26987e..8cc2f58 100644 --- a/src/screens/home/UserProfileStatus.tsx +++ b/src/screens/home/UserProfileStatus.tsx @@ -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/management/ManagementScreen.tsx b/src/screens/management/ManagementScreen.tsx index f3421ca..9e2f5df 100644 --- a/src/screens/management/ManagementScreen.tsx +++ b/src/screens/management/ManagementScreen.tsx @@ -14,86 +14,45 @@ 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") { - setUserRole(json.data.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) - if (userRole === "ORGANIZER") { + console.log(" abin user : ", userRole) + if (userRole.role === "ORGANIZER") { // Fetch all data concurrently 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>, ]); @@ -105,10 +64,14 @@ const ManagementScreen: FC = () => { setRequests(requestsRes.data || []); } else { // Fetch only joined groups for non-organizers - const [joinedGroupsRes] = await Promise.all([ + const [joinedGroupsRes, userRequest] = await Promise.all([ apiCall(GET_JOINED_GROUP_URL, "GET") as Promise>, // Assuming this is also needed + apiCall(USER_REQUEST, "GET") as Promise>, // Assuming this is also needed ]); - setJoinedGroups(joinedGroupsRes.data || []); + setJoinedGroups(joinedGroupsRes.data || []); + setUserRequest(userRequest?.data || []); + console.log("joinedGroupsRes data : ", joinedGroupsRes) + console.log("user request data : ", userRequest) } } catch (e) { console.error("Fetch Data Error:", e); @@ -197,12 +160,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/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/ApiUrls.tsx b/src/utils/ApiUrls.tsx index ac4d6b3..37b43a7 100644 --- a/src/utils/ApiUrls.tsx +++ b/src/utils/ApiUrls.tsx @@ -15,3 +15,5 @@ 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` 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); +};