diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6b0e5ab --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "postman.settings.dotenv-detection-notification-visibility": false +} \ No newline at end of file diff --git a/src/assets/images/management.png b/src/assets/images/management.png new file mode 100644 index 0000000..865e966 Binary files /dev/null and b/src/assets/images/management.png differ diff --git a/src/assets/images/management_1.png b/src/assets/images/management_1.png new file mode 100644 index 0000000..c3b4531 Binary files /dev/null and b/src/assets/images/management_1.png differ diff --git a/src/components/JoinedGroupCard.tsx b/src/components/JoinedGroupCard.tsx new file mode 100644 index 0000000..ba68137 --- /dev/null +++ b/src/components/JoinedGroupCard.tsx @@ -0,0 +1,43 @@ +// src/components/JoinedGroupCard.tsx + +import React, { FC } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { JoinedGroup } from '../types'; + +interface JoinedGroupCardProps { + item: JoinedGroup; +} + +export const JoinedGroupCard: FC = ({ item }) => ( + + + {item.groupName} + Created by {item.createdBy} + + +); + +const styles = StyleSheet.create({ + card: { + backgroundColor: '#fff', + borderRadius: 12, + padding: 16, + marginBottom: 12, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + elevation: 2, + shadowColor: '#000', + shadowOpacity: 0.05, + shadowRadius: 4, + }, + cardTitle: { + fontSize: 17, + fontWeight: 'bold', + color: '#343a40', + }, + cardSubtitle: { + fontSize: 14, + color: '#6c757d', + }, +}); diff --git a/src/components/MyGroupCard.tsx b/src/components/MyGroupCard.tsx new file mode 100644 index 0000000..c72c3e2 --- /dev/null +++ b/src/components/MyGroupCard.tsx @@ -0,0 +1,164 @@ +// components/MyGroupCard.tsx +import React from "react"; +import { + View, + Text, + StyleSheet, + TouchableOpacity, +} from "react-native"; +import { Ionicons } from "@react-native-vector-icons/ionicons"; +import colors from "./colors"; + +interface MyGroup { + id: string; + groupName: string; + description: string; + visibility: "PUBLIC" | "PRIVATE"; + createdByName: string; + currentParticipants: number; + maxParticipants: number; + contributionAmount: number; + cycleType: "MONTHLY" | "QUARTERLY" | "YEARLY"; + status: "PENDING" | "ACTIVE" | "COMPLETED"; +} + +interface Props { + item: MyGroup; + onManage: (group: MyGroup) => void; +} + +export const MyGroupCard: React.FC = ({ item, onManage }) => { + const getStatusColor = () => { + switch (item.status) { + case "ACTIVE": + return colors.pastelGreen; + case "PENDING": + return colors.pastelOrange; // amber + case "COMPLETED": + return colors.pastelBlue; + default: + return "#adb5bd"; + } + }; + + return ( + + {/* Left Content */} + + + + {item.groupName} + + + {item.status === "ACTIVE" ? "Active" : "Not Active"} + + + + + {item.description} + + + + + {item.currentParticipants}/{item.maxParticipants} + + + ₹{item.contributionAmount} + + + {item.cycleType} + + + + Created by {item.createdByName} + + + {/* Manage Button */} + onManage(item)}> + + Manage + + + ); +}; + +const styles = StyleSheet.create({ + card: { + backgroundColor: "#fff", + borderRadius: 12, + padding: 16, + marginBottom: 12, + flexDirection: "row", + alignItems: "center", + elevation: 2, + shadowColor: "#000", + shadowOpacity: 0.05, + shadowRadius: 4, + }, + cardHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: 6, + }, + cardTitle: { + fontSize: 17, + fontWeight: "bold", + color: "#343a40", + flex: 1, + marginRight: 10, + }, + cardSubtitle: { + fontSize: 14, + color: "#6c757d", + marginBottom: 8, + }, + cardMetaContainer: { + flexDirection: "row", + alignItems: "center", + flexWrap: "wrap", + marginBottom: 6, + }, + manageButtonText: { + fontSize: 12, + color: colors.textDark, + }, + cardMetaText: { + fontSize: 13, + color: "#495057", + fontWeight: "500", + marginRight: 16, + flexDirection: "row", + alignItems: "center", + }, + statusBadge: { + paddingHorizontal: 10, + paddingVertical: 4, + borderRadius: 5, + elevation: 1, + }, + statusBadgeText: { + color: colors.textDark, + fontSize: 10, + fontWeight: "bold", + textTransform: "uppercase", + }, + manageButton: { + width: 100, + borderRadius:10, + flexDirection:"row", + alignItems: "center", + justifyContent: "space-around", + padding: 6, + marginLeft: 10, + elevation:2, + backgroundColor: colors.pastelOrange, + }, + createdBy: { + fontSize: 12, + color: "#868e96", + marginTop: 4, + }, +}); + +export default MyGroupCard; diff --git a/src/components/RequestCard.tsx b/src/components/RequestCard.tsx new file mode 100644 index 0000000..57eb7a2 --- /dev/null +++ b/src/components/RequestCard.tsx @@ -0,0 +1,139 @@ +// src/components/RequestCard.tsx + +import React, { FC } from 'react'; +import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native'; +import { Ionicons } from "@react-native-vector-icons/ionicons"; +import colors from './colors'; // Assuming your colors are defined here +import { JoinRequest } from '../types'; + +interface RequestCardProps { + item: JoinRequest; + onApprove: (req: JoinRequest) => void; + onDecline: (req: JoinRequest) => void; +} + +export const RequestCard: FC = ({ item, onApprove, onDecline }) => { + return ( + + {/* Top section with user avatar and info */} + + + + {item.userName} + {item.mobile} + + + + {/* Middle section for the group name */} + + + Wants to join the group: {item.groupName} + + + + {/* Bottom section with action buttons */} + + onDecline(item)}> + + Decline + + onApprove(item)}> + {/* Corrected icon color and updated text color for better contrast */} + + Approve + + + + ); +}; + +// 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: { + backgroundColor: colors.white, + borderRadius: 16, + marginVertical: 8, + elevation: 4, + shadowColor: '#000', + shadowOpacity: 0.1, + shadowRadius: 10, + shadowOffset: { width: 0, height: 4 }, + padding: 8, + }, + userInfoSection: { + flexDirection: 'row', + alignItems: 'center', + borderBottomWidth: 1, + borderBottomColor: colors.border, + paddingBottom: 12, + }, + avatar: { + width: 50, + height: 50, + borderRadius: 25, + marginRight: 12, + }, + userInfo: { + flex: 1, + }, + userName: { + fontSize: 17, + fontWeight: 'bold', + color: colors.textDark, + }, + userMeta: { + fontSize: 14, + color: colors.textLight, + marginTop: 4, + }, + groupInfoSection: { + paddingVertical: 16, + }, + requestText: { + fontSize: 15, + color: colors.textDark, + lineHeight: 22, + }, + groupName: { + fontWeight: 'bold', + color: colors.textDark, + }, + actionsSection: { + flexDirection: 'row', + justifyContent: 'space-between', + gap: 12, + }, + actionButton: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 12, + borderRadius: 12, + }, + declineButton: { + backgroundColor: colors.lightDanger, + }, + approveButton: { + // Set background color as requested + backgroundColor: colors.pastelOrange, + }, + actionButtonText: { + fontSize: 16, + fontWeight: '600', + marginLeft: 8, + }, +}); diff --git a/src/navigation/BottomTabs.tsx b/src/navigation/BottomTabs.tsx index c2cfcf1..5fe70ea 100644 --- a/src/navigation/BottomTabs.tsx +++ b/src/navigation/BottomTabs.tsx @@ -7,6 +7,7 @@ import Profile from "../screens/profile/Profile"; import Home from "../screens/home/Home"; import colors from "../components/colors"; import Setting from "../screens/setting/Setting"; +import ManagementScreen from "../screens/management/ManagementScreen"; const Tab = createBottomTabNavigator(); @@ -23,7 +24,7 @@ const activeTabColors: { [key: string]: string } = { Home: colors.pastelPink, // Soft pink Profile: colors.pastelBlue, // Light sky blue Group: colors.pastelGreen, // Gentle mint green - Settings: colors.pastelOrange, // Warm pastel yellow + Management: colors.pastelOrange, // Warm pastel yellow }; // Icon/Text colors on active tabs @@ -31,7 +32,7 @@ const activeIconColors: { [key: string]: string } = { Home: "#333", Profile: "#333", Group: "#333", - Settings: "#333", + Management: "#333", } @@ -65,7 +66,7 @@ export default function BottomTabs() { // Determine icon based on route if (route.name === "Home") iconName = focused ? "home" : "home-outline"; else if (route.name === "Profile") iconName = focused ? "person" : "person-outline"; - else if (route.name === "Settings") iconName = focused ? "settings" : "settings-outline"; + else if (route.name === "Management") iconName = focused ? "settings" : "settings-outline"; else if (route.name === "Group") iconName = focused ? "people" : "people-outline"; // 3. Get the active background color and icon color from the maps @@ -83,11 +84,11 @@ export default function BottomTabs() { ], ]} > - + />}:null) ); }, @@ -96,7 +97,7 @@ export default function BottomTabs() { - + ); } diff --git a/src/screens/GroupCreation.tsx b/src/screens/GroupCreation.tsx index 0425d37..7ed8224 100644 --- a/src/screens/GroupCreation.tsx +++ b/src/screens/GroupCreation.tsx @@ -109,7 +109,7 @@ export default function GroupCreationForm() { Group Creation - handleChange("groupName", val)} icon="people" error={errors.groupName} /> + handleChange("groupName", val)} icon="people" /> handleChange("description", val)} multiline icon="document-text" error={errors.description} /> Visibility @@ -124,8 +124,8 @@ export default function GroupCreationForm() { - handleChange("minParticipants", val)} keyboardType="numeric" icon="person-add" error={errors.minParticipants} /> - handleChange("maxParticipants", val)} keyboardType="numeric" icon="people-circle" error={errors.maxParticipants} /> + handleChange("minParticipants", val)} keyboardType="numeric" icon="person-add" /> + handleChange("maxParticipants", val)} keyboardType="numeric" icon="people-circle" /> Contribution Amount (INR) @@ -134,14 +134,14 @@ export default function GroupCreationForm() { - handleChange("contributionAmount", val)} keyboardType="numeric" icon="cash" error={errors.contributionAmount} /> + handleChange("contributionAmount", val)} keyboardType="numeric" icon="cash" /> Cycle Type {cycleTypes.map((type) => ( handleChange("cycleType", type)} style={[styles.cycleTypeButton, form.cycleType === type && styles.cycleTypeSelected]}>{type.charAt(0) + type.slice(1).toLowerCase()}))} - {form.cycleType === "CUSTOM" && ( handleChange("customCycleDays", val)} keyboardType="numeric" icon="calendar" error={errors.customCycleDays} />)} + {form.cycleType === "CUSTOM" && ( handleChange("customCycleDays", val)} keyboardType="numeric" icon="calendar" />)} Start Date setCalendarVisible(true)}> diff --git a/src/screens/GroupScreen.tsx b/src/screens/GroupScreen.tsx index 033e56c..d8341a6 100644 --- a/src/screens/GroupScreen.tsx +++ b/src/screens/GroupScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { View, Text, @@ -20,8 +20,10 @@ import { StackNavigationProp } from "@react-navigation/stack"; import { RootStackParamList } from "../navigation/AppNavigator"; import AppView from "../components/AppView"; import { apiCall } from "../utils/ApiHelper"; -import { JOIN_GROUP_URL } from "../utils/ApiUrls"; - +import { BASE_URL, JOIN_GROUP_URL } from "../utils/ApiUrls"; +import colors from "../components/colors"; +import { ApiResponse } from "./home/UserProfileStatus"; +import * as Keychain from "react-native-keychain"; const { width } = Dimensions.get('window'); const PUBLIC_GROUPS_URL = "https://api.godemo.in/FundCollect/api/fund/user/public"; @@ -89,6 +91,7 @@ export default function GroupScreen() { const [featuredGroups, setFeaturedGroups] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [role, setRole] = useState(null); const fetchGroups = async () => { if (loading) return; @@ -137,7 +140,50 @@ export default function GroupScreen() { Alert.alert("Error", "An unexpected error occurred."); } }; + 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: ApiResponse = await response.json(); + + console.log("👤 Profile Status Response:", json); + if (json.status === "success") { + setRole(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 handleViewGroup = (group: Group) => { // navigation.navigate('GroupDetails', { group }); @@ -205,11 +251,16 @@ export default function GroupScreen() { - {["My Groups", "Joined", "Public"].map((tab) => ( + + {role === "ORGANIZER" ? (["My Groups", "Joined", "Public"].map((tab) => ( setActiveTab(tab)}> {tab} - ))} + ))) : (["Joined", "Public"].map((tab) => ( + setActiveTab(tab)}> + {tab} + + ))) } setIsGridView(!isGridView)} style={styles.layoutToggleButton}> @@ -219,7 +270,9 @@ export default function GroupScreen() { {/* The list now renders inside a simple View, as FlatList handles its own scroll */} {renderContent()} - } onPress={() => navigation.navigate('GroupCreation')} /> + {role === "ORGANIZER" && ( + } onPress={() => navigation.navigate('GroupCreation')} /> + )} ); } @@ -241,12 +294,12 @@ const styles = StyleSheet.create({ tabHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, marginBottom: 16 }, segmentedControl: { flex: 1, flexDirection: 'row', backgroundColor: '#e9ecef', borderRadius: 10, padding: 4, marginRight: 10 }, segmentButton: { flex: 1, paddingVertical: 10, borderRadius: 8, alignItems: 'center' }, - segmentButtonActive: { backgroundColor: '#fff', elevation: 2, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 }, + segmentButtonActive: { backgroundColor: colors.pastelGreen, elevation: 2, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 }, segmentText: { fontSize: 14, fontWeight: '600', color: '#555' }, segmentTextActive: { color: '#333' }, layoutToggleButton: { padding: 8 }, contentArea: { paddingHorizontal: 16 }, - fab: { position: "absolute", right: 20, bottom: 90, width: 60, height: 60, justifyContent: "center", alignItems: "center", backgroundColor: "#83f5cc", borderRadius: 30, elevation: 4 }, + fab: { position: "absolute", right: 20, bottom: 90, width: 60, height: 60, justifyContent: "center", alignItems: "center", backgroundColor: colors.pastelGreen, borderRadius: 30, elevation: 4 }, listCard: { backgroundColor: '#fff', padding: 16, borderRadius: 12, marginBottom: 12, elevation: 1 }, gridCard: { backgroundColor: '#fff', padding: 12, borderRadius: 12, marginBottom: 12, elevation: 1, width: (width - 48) / 2 }, groupName: { fontSize: 16, fontWeight: 'bold', color: '#333' }, @@ -258,7 +311,7 @@ const styles = StyleSheet.create({ buttonContainer: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 8 }, viewButton: { flex: 1, backgroundColor: '#e9ecef', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginRight: 5 }, viewButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 }, - joinButton: { flex: 1, backgroundColor: '#83f5cc', paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginLeft: 5 }, + joinButton: { flex: 1, backgroundColor: colors.pastelGreen, paddingVertical: 10, borderRadius: 8, alignItems: 'center', marginLeft: 5 }, joinButtonDisabled: { backgroundColor: '#bdc3c7' }, joinButtonText: { color: '#333', fontWeight: 'bold', fontSize: 14 }, }); diff --git a/src/screens/home/UserProfileStatus.tsx b/src/screens/home/UserProfileStatus.tsx index d3f4a4d..30a5676 100644 --- a/src/screens/home/UserProfileStatus.tsx +++ b/src/screens/home/UserProfileStatus.tsx @@ -136,7 +136,13 @@ const UserProfileStatus: React.FC = () => { setShowModal(false)} + onPress={() => { if (intervalId) { + clearInterval(intervalId); + setIntervalId(null); + } + + setShowModal(false); + }} > Close diff --git a/src/screens/management/ManagementScreen.tsx b/src/screens/management/ManagementScreen.tsx new file mode 100644 index 0000000..be84417 --- /dev/null +++ b/src/screens/management/ManagementScreen.tsx @@ -0,0 +1,230 @@ +// src/screens/ManagementScreen.tsx + +import React, { useState, useCallback, FC } from "react"; +import { + View, + Text, + StyleSheet, + FlatList, + TouchableOpacity, + ActivityIndicator, + Alert, + ListRenderItemInfo, +} from "react-native"; +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, MY_GROUPS_URL, REQUESTS_URL } from "../../utils/ApiUrls"; +import colors from "../../components/colors"; +import AppView from "../../components/AppView"; +import { apiCall } from "../../utils/ApiHelper"; + +import { MyGroup, JoinedGroup, JoinRequest, ApiResponse } from '../../types'; +import { MyGroupCard } from "../../components/MyGroupCard"; +import { JoinedGroupCard } from "../../components/JoinedGroupCard"; +import { RequestCard } from "../../components/RequestCard"; + +type GroupManagementNavProp = StackNavigationProp; +type ActiveTab = "My Groups" | "Joined Groups" | "Requests"; + +const ManagementScreen: FC = () => { + const [activeTab, setActiveTab] = useState("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 = "ORGANIZER"; + + const fetchData = useCallback(async () => { + if (loading) return; + setLoading(true); + setError(null); + try { + if (userRole === "ORGANIZER") { + // Fetch all data concurrently + const [myGroupsRes,joinedGroupsRes,requestsRes] = await Promise.all([ + apiCall(MY_GROUPS_URL, "GET") as Promise>, + apiCall(DECLINE_REQUEST_URL, "GET") as Promise>, // Assuming this is also needed + apiCall(REQUESTS_URL, "GET") as Promise>, + ]); + setMyGroups(myGroupsRes.data || []); + setJoinedGroups(joinedGroupsRes.data || []); + setRequests(requestsRes.data || []); + } else { + // Fetch only joined groups for non-organizers + // const joinedGroupsRes = await apiCall(JOINED_GROUPS_URL, "GET") as Promise>; + // setJoinedGroups(joinedGroupsRes.data || []); + } + } catch (e) { + console.error("Fetch Data Error:", e); + setError("Failed to fetch data. Please pull to refresh."); + } finally { + setLoading(false); + } + }, [userRole, loading]); + + useFocusEffect(useCallback(() => { fetchData(); }, [])); + + const handleAction = async (url: string, body: object, successMessage: string, failureMessage: string, action: string) => { + // ... (logic remains the same) + console.log(url) + try { + setLoading(true); // Optionally show a loading state during the action + // Make the POST request with an empty body + const res = await apiCall(url, "PUT", {}) as ApiResponse | null; + + if (res && res.statusCode === 200) { + // Use the success message from the API response + Alert.alert("Success", res.message || `Request ${action}d successfully.`); + fetchData(); // Refresh the list of requests + } else { + throw new Error(res?.message || failureMessage); + } + } catch (err: any) { + Alert.alert("Error", err.message || "An unexpected error occurred."); + } finally { + setLoading(false); + } + }; + + const handleApprove = (req: JoinRequest) => handleAction(APPROVE_REQUEST_URL+req.requestId+"/approve", { requestId: req.requestId }, `${req.userName} has been added.`, "Failed to approve request.","Approve"); + const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL+req.requestId+"/reject?reason=/"+" ", { requestId: req.requestId }, `Request from ${req.userName} has been declined.`, "Failed to decline request.","Decline"); + const handleManageGroup = (group: MyGroup) => Alert.alert("Manage Group", `Navigating to manage ${group.groupName}`); + + const renderContent = () => { + if (error) { + return {error}; + } + + switch (activeTab) { + case "My Groups": + if (loading) { + return ; + } + return ( + ) => } + keyExtractor={(item) => item.id} + ListEmptyComponent={You haven't created any groups yet.} + onRefresh={fetchData} + refreshing={loading} // This handles the "continuing loading" on pull-to-refresh + contentContainerStyle={{ paddingBottom: 100 }} + /> + ); + case "Joined Groups": + if (loading && joinedGroups.length === 0) { + return ; + } + return ( + ) => } + keyExtractor={(item) => item.id} + ListEmptyComponent={You haven't joined any groups yet.} + onRefresh={fetchData} + refreshing={loading} // This handles the "continuing loading" on pull-to-refresh + contentContainerStyle={{ paddingBottom: 100 }} + /> + ); + case "Requests": + if (loading && requests.length === 0) { + return ; + } + return ( + ) => } + keyExtractor={(item) => item.requestId} + ListEmptyComponent={No pending join requests.} + onRefresh={fetchData} + refreshing={loading} // This handles the "continuing loading" on pull-to-refresh + contentContainerStyle={{ paddingBottom: 100 }} + /> + ); + default: return null; + } + }; + + const tabs: ActiveTab[] = userRole === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups"]; + + return ( + + + Group Management + + + {tabs.map((tab) => ( + setActiveTab(tab)}> + {tab} + + ))} + + + {renderContent()} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#f5f6fa' + }, + header: { + paddingHorizontal: 20, + paddingBottom: 15, + }, + title: { + fontSize: 28, + fontWeight: 'bold', + color: '#1d2129', + }, + segmentedControl: { + flexDirection: 'row', + backgroundColor: '#e9ecef', + borderRadius: 10, + marginHorizontal: 20, + marginBottom: 20, + padding: 3, + }, + segmentButton: { + flex: 1, + paddingVertical: 12, + alignItems: 'center', + borderRadius: 8, + }, + segmentActive: { + backgroundColor: colors.pastelOrange, // Example color + elevation: 3, + shadowColor: '#000', + shadowOpacity: 0.1, + shadowRadius: 5, + }, + segmentText: { + fontSize: 14, + fontWeight: '600', + color: '#6c757d', + }, + segmentTextActive: { + color: colors.textDark, // Example color + }, + contentArea: { + flex: 1, + paddingHorizontal: 20, + }, + loader: { + marginTop: 50, + }, + emptyText: { + textAlign: 'center', + marginTop: 60, + fontSize: 16, + color: '#6c757d', + }, +}); + +export default ManagementScreen; diff --git a/src/screens/profile/Profile.tsx b/src/screens/profile/Profile.tsx index 90f5e83..a219896 100644 --- a/src/screens/profile/Profile.tsx +++ b/src/screens/profile/Profile.tsx @@ -9,21 +9,20 @@ import { Alert, ActivityIndicator, StatusBar, - TextInput, KeyboardAvoidingView, Platform, + RefreshControl, } from "react-native"; import * as Keychain from "react-native-keychain"; -import { Ionicons } from "@react-native-vector-icons/ionicons"; +import { Ionicons } from "@react-native-vector-icons/ionicons"; // Assuming you use expo-cli or have this package import { launchImageLibrary } from "react-native-image-picker"; import { CustomButton, CustomInput } from "../../components/common/Custom"; import colors from "../../components/colors"; import SwitchNavigator from "./SwitchNavigator"; -import { useFocusEffect } from "@react-navigation/native"; +import { CommonActions, useFocusEffect, useNavigation } from "@react-navigation/native"; import { ApiResponse } from "../home/UserProfileStatus"; import { BASE_URL } from "../../utils/ApiUrls"; - - +import AsyncStorage from "@react-native-async-storage/async-storage"; type UserProfile = { id: string; @@ -50,10 +49,12 @@ const Profile = () => { const [editing, setEditing] = useState(false); const [imageUpdating, setImageUpdating] = useState(false); const [loading, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(false); // State for pull-to-refresh const [statusData, setStatusData] = useState(null); const [error, setError] = useState(null); + const navigation = useNavigation(); - const fetchUserProfile = async () => { + const fetchUserProfile = async (isRefreshing = false) => { try { const credentials = await Keychain.getGenericPassword(); if (!credentials) { @@ -62,17 +63,17 @@ const Profile = () => { } const token = credentials.password; - setFetching(true); - const response = await fetch( - `${BASE_URL}user/profile/view`, - { - method: "GET", - headers: { - Accept: "*/*", - Authorization: `Bearer ${token}`, - }, - } - ); + if (!isRefreshing) { + setFetching(true); + } + + const response = await fetch(`${BASE_URL}user/profile/view`, { + method: "GET", + headers: { + Accept: "*/*", + Authorization: `Bearer ${token}`, + }, + }); const result = await response.json(); if (result.status === "success") { @@ -84,7 +85,10 @@ const Profile = () => { console.error("❌ Error fetching profile:", error); Alert.alert("Error", "Could not connect to server"); } finally { - setFetching(false); + if (!isRefreshing) { + setFetching(false); + } + setRefreshing(false); } }; @@ -97,25 +101,22 @@ const Profile = () => { } const token = credentials.password; - const response = await fetch( - `${BASE_URL}user/profile/update`, - { - method: "PUT", - headers: { - Accept: "*/*", - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify({ - address: user?.address || null, - email: user?.email || null, - firstName: user?.firstName || null, - gender: user?.gender || null, - lastName: user?.lastName || null, - username: user?.username || null, - }), - } - ); + const response = await fetch(`${BASE_URL}user/profile/update`, { + method: "PUT", + headers: { + Accept: "*/*", + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ + address: user?.address || null, + email: user?.email || null, + firstName: user?.firstName || null, + gender: user?.gender || null, + lastName: user?.lastName || null, + username: user?.username || null, + }), + }); const result = await response.json(); if (result.status === "success") { @@ -168,31 +169,23 @@ const Profile = () => { name: "profile.jpg", } as any); - const response = await fetch( - `${BASE_URL}user/update/userImg`, - { - method: "PUT", - headers: { - Accept: "application/json", - Authorization: `Bearer ${token}`, - }, - body: formData, - } - ); + const response = await fetch(`${BASE_URL}user/update/userImg`, { + method: "PUT", + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + body: formData, + }); const result = await response.json(); if (result.status === "success") { Alert.alert("Success", "Profile image updated successfully"); - - // If API returns updated image, set it if (result.data?.imageBlob) { setUser((prev) => prev ? { ...prev, imageBlob: result.data.imageBlob, filePath: null } : prev); } else { - // Otherwise, keep the local filePath until next fetch setUser((prev) => prev ? { ...prev, filePath: user.filePath } : prev); } - - // Refresh profile from API as well fetchUserProfile(); } else { Alert.alert("Error", result.message || "Failed to update profile image"); @@ -225,11 +218,9 @@ const Profile = () => { }); const json: ApiResponse = await response.json(); - console.log("👤 Profile Status Response:", json); if (json.status === "success") { setStatusData(json.data); - } else { setError(json.message || "Failed to fetch status"); } @@ -240,19 +231,52 @@ const Profile = () => { } }; + const handleLogout = async () => { + Alert.alert( + "Confirm Logout", + "Are you sure you want to log out?", + [ + { text: "Cancel", style: "cancel" }, + { + text: "Logout", + style: "destructive", + onPress: async () => { + try { + await AsyncStorage.clear(); + await Keychain.resetGenericPassword(); + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [{ name: "Splash" }], + }) + ); + } catch (error) { + console.log("Logout error:", error); + Alert.alert("Error", "Could not log out. Please try again."); + } + }, + }, + ] + ); + }; + + const onRefresh = useCallback(() => { + setRefreshing(true); + fetchUserProfile(true).then(() => { + userStatus(); + }); + }, []); + useFocusEffect( useCallback(() => { userStatus(); }, []) ); - console.log("login user data :", user); - useEffect(() => { fetchUserProfile(); }, []); - console.log("user image :", user?.filePath); const getProfileImage = () => { if (user?.imageBlob) { return `data:image/jpeg;base64,${user.imageBlob}`; @@ -263,7 +287,6 @@ const Profile = () => { return "https://randomuser.me/api/portraits/men/1.jpg"; }; - if (fetching || !user) { return ( @@ -277,13 +300,18 @@ const Profile = () => { - {/* Header */} User Profile - - - + + fetchUserProfile(true)} style={styles.refreshButton}> + + + + + + + { + } > - - {/* Profile Card */} - {/* Profile Image Section */} - { - statusData?.role && statusData?.organizerStatus ? - Your Role: {statusData.role} | Organizer Status: {statusData.organizerStatus} - : - } + {statusData?.role && statusData?.organizerStatus ? ( + + Your Role: {statusData.role} | Organizer Status: {statusData.organizerStatus} + + ) : ( + + )} { - {!editing ? ( <> - {/* Profile Details */} Personal Information - - - Become Organizer - - @@ -340,7 +367,6 @@ const Profile = () => { {user.firstName} {user.lastName} - @@ -348,7 +374,6 @@ const Profile = () => { {user.email || "Not provided"} - @@ -356,7 +381,6 @@ const Profile = () => { {user.mobile || "Not provided"} - @@ -364,7 +388,6 @@ const Profile = () => { {user.username} - @@ -372,7 +395,6 @@ const Profile = () => { {user.address || "Not provided"} - @@ -380,7 +402,6 @@ const Profile = () => { {user.gender || "Not specified"} - @@ -389,7 +410,6 @@ const Profile = () => { - setEditing(true)} @@ -400,10 +420,8 @@ const Profile = () => { ) : ( <> - {/* Edit Form */} Edit Profile - { onChangeText={(text) => setUser({ ...user, firstName: text })} icon="person" /> - { onChangeText={(text) => setUser({ ...user, lastName: text })} icon="person" /> - { icon="mail" keyboardType="email-address" /> - { const styles = StyleSheet.create({ container: { flex: 1, - // backgroundColor: colors.pastelBlue, paddingBottom: '30%', }, loadingContainer: { flex: 1, justifyContent: "center", alignItems: "center", - backgroundColor: "#f8fafc" + backgroundColor: "#f8fafc", }, loadingText: { marginTop: 10, fontSize: 16, - color: "#64748b" + color: "#64748b", }, header: { flexDirection: "row", @@ -500,24 +514,23 @@ const styles = StyleSheet.create({ alignItems: "center", padding: 16, paddingTop: 50, - // backgroundColor: "#6366f1", - borderBottomLeftRadius: 20, - borderBottomRightRadius: 20, - // elevation: 4, - // shadowColor: "#000", - // shadowOffset: { width: 0, height: 2 }, - // shadowOpacity: 0.2, - // shadowRadius: 4, }, headerTitle: { fontSize: 22, fontWeight: "700", - color: "#000" + color: "#000", + }, + headerIcons: { + flexDirection: 'row', + alignItems: 'center', + gap: 16, }, refreshButton: { padding: 4, }, - + logoutButton: { + padding: 4, + }, profileCard: { borderRadius: 20, margin: 16, @@ -602,7 +615,6 @@ const styles = StyleSheet.create({ }, editSection: { marginBottom: 20, - flex: 1, }, editButtons: { gap: 12, @@ -613,79 +625,7 @@ const styles = StyleSheet.create({ cancelButton: { marginTop: 4, }, - inputContainer: { - marginBottom: 16, - }, - inputLabel: { - fontSize: 14, - fontWeight: "600", - color: "#374151", - marginBottom: 6, - }, - inputWrapper: { - flexDirection: "row", - alignItems: "center", - position: "relative", - }, - inputIcon: { - position: "absolute", - left: 12, - zIndex: 1, - }, - input: { - borderWidth: 1, - borderColor: "#d1d5db", - borderRadius: 12, - padding: 12, - backgroundColor: "#fff", - color: "#1f2937", - fontSize: 16, - flex: 1, - }, - inputWithIcon: { - paddingLeft: 40, - }, - button: { - backgroundColor: "#6366f1", - paddingVertical: 14, - borderRadius: 12, - alignItems: "center", - justifyContent: "center", - flexDirection: "row", - shadowColor: "#6366f1", - shadowOpacity: 0.3, - shadowRadius: 6, - shadowOffset: { width: 0, height: 3 }, - elevation: 4, - }, - buttonSecondary: { - backgroundColor: "#f1f5f9", - }, - buttonDanger: { - backgroundColor: "#ef4444", - }, - buttonOutline: { - backgroundColor: "transparent", - borderWidth: 1, - borderColor: "#6366f1", - }, - buttonText: { - color: "#fff", - fontSize: 16, - fontWeight: "600", - }, - buttonTextSecondary: { - color: "#334155", - }, - buttonTextDanger: { - color: "#fff", - }, - buttonTextOutline: { - color: "#6366f1", - }, - buttonIcon: { - marginRight: 8, - }, + // Add any other styles you might need }); -export default Profile; \ No newline at end of file +export default Profile; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..ac177c9 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,38 @@ +// src/types.ts + +export interface ApiResponse { + status: "success" | "error"; + message: string; + data?: T; + statusCode: number; +} + +export interface MyGroup { + id: string; + groupName: string; + description: string; + visibility: "PUBLIC" | "PRIVATE"; + createdByName: string; + currentParticipants: number; + maxParticipants: number; + contributionAmount: number; + cycleType: "MONTHLY" | "QUARTERLY" | "YEARLY"; + status: "PENDING" | "ACTIVE" | "COMPLETED"; +} + +export interface JoinedGroup { + id: string; + groupName: string; + createdBy: string; +} + +export interface JoinRequest { + requestId: string; + groupId: string; + groupName: string; + userId: number; + userName: string; + mobile: string; + status: "PENDING"; + userAvatar?: string; +} diff --git a/src/utils/ApiUrls.tsx b/src/utils/ApiUrls.tsx index bc2d192..f79ac04 100644 --- a/src/utils/ApiUrls.tsx +++ b/src/utils/ApiUrls.tsx @@ -6,4 +6,9 @@ export const GROUP_CREATION_URL = `${BASE_URL}fund/groups/{organizerId}` export const PROFILE_URL =`${BASE_URL}user/profile/view` export const PUBLIC_GROUPS_URL =`${BASE_URL}fund/user/public` export const JOIN_GROUP_URL = `${BASE_URL}fund`; -//https://api.godemo.in/FundCollect/api/fund/groups/2a80cdaa-8532-4b8a-87f0-9c1023d18a7f/join/64 \ No newline at end of file +export const MY_GROUPS_URL =`https://api.godemo.in/FundCollect/api/fund/getAll/my/groups` + +export const REQUESTS_URL =`https://api.godemo.in/FundCollect/api/fund/group/pending/request` +//https://api.godemo.in/FundCollect/api/fund/groups/2a80cdaa-8532-4b8a-87f0-9c1023d18a7f/join/64 +export const APPROVE_REQUEST_URL =`https://api.godemo.in/FundCollect/api/fund/requests/` +export const DECLINE_REQUEST_URL =`https://api.godemo.in/FundCollect/api/fund/user/my/groups` \ No newline at end of file