Merge branch 'feature_profile' of https://git.zeptic.in/abintoppo_lincpay/CircleCollect into feature_pending
This commit is contained in:
@@ -28,10 +28,6 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="myapp" android:host="invite"/>
|
<data android:scheme="myapp" android:host="invite"/>
|
||||||
|
|
||||||
<data android:scheme="http"
|
|
||||||
android:host="192.168.1.19"
|
|
||||||
android:pathPrefix="/invite" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|||||||
Generated
+24
@@ -8,6 +8,7 @@
|
|||||||
"name": "CircleCollect",
|
"name": "CircleCollect",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-native-clipboard/clipboard": "^1.16.3",
|
||||||
"@react-native-community/slider": "^5.0.1",
|
"@react-native-community/slider": "^5.0.1",
|
||||||
"@react-native-picker/picker": "^2.11.2",
|
"@react-native-picker/picker": "^2.11.2",
|
||||||
"@react-native-vector-icons/ionicons": "^12.3.0",
|
"@react-native-vector-icons/ionicons": "^12.3.0",
|
||||||
@@ -2740,6 +2741,29 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-clipboard/clipboard": {
|
||||||
|
"version": "1.16.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.16.3.tgz",
|
||||||
|
"integrity": "sha512-cMIcvoZKIrShzJHEaHbTAp458R9WOv0fB6UyC7Ek4Qk561Ow/DrzmmJmH/rAZg21Z6ixJ4YSdFDC14crqIBmCQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"example"
|
||||||
|
],
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.9.0",
|
||||||
|
"react-native": ">= 0.61.5",
|
||||||
|
"react-native-macos": ">= 0.61.0",
|
||||||
|
"react-native-windows": ">= 0.61.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-native-macos": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-native-windows": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native-community/cli": {
|
"node_modules/@react-native-community/cli": {
|
||||||
"version": "20.0.0",
|
"version": "20.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-20.0.0.tgz",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@react-native-clipboard/clipboard": "^1.16.3",
|
||||||
"@react-native-community/slider": "^5.0.1",
|
"@react-native-community/slider": "^5.0.1",
|
||||||
"@react-native-picker/picker": "^2.11.2",
|
"@react-native-picker/picker": "^2.11.2",
|
||||||
"@react-native-vector-icons/ionicons": "^12.3.0",
|
"@react-native-vector-icons/ionicons": "^12.3.0",
|
||||||
|
|||||||
+105810
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,7 @@ interface JoinedGroup {
|
|||||||
|
|
||||||
interface JoinedGroupCardProps {
|
interface JoinedGroupCardProps {
|
||||||
item: JoinedGroup | any;
|
item: JoinedGroup | any;
|
||||||
|
onViewPress: () => void; // Function prop for the View button
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to format dates
|
// Helper function to format dates
|
||||||
@@ -60,7 +61,7 @@ const getStatusStyle = (status: JoinedGroup['status']) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }) => {
|
export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item, onViewPress }) => {
|
||||||
const progress = (item.currentParticipants / item.maxParticipants) * 100;
|
const progress = (item.currentParticipants / item.maxParticipants) * 100;
|
||||||
const statusStyle = getStatusStyle(item.status);
|
const statusStyle = getStatusStyle(item.status);
|
||||||
const navigation = useNavigation<any>()
|
const navigation = useNavigation<any>()
|
||||||
@@ -99,11 +100,14 @@ export const JoinedGroupCard: FC<JoinedGroupCardProps> = ({ item }) => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Dates */}
|
{/* Footer with Dates and View Button */}
|
||||||
<View style={styles.footer}>
|
<View style={styles.footer}>
|
||||||
<Text style={styles.dateText}>
|
<Text style={styles.dateText}>
|
||||||
{formatDate(item.startDate)} - {formatDate(item.endDate)}
|
{formatDate(item.startDate)} - {formatDate(item.endDate)}
|
||||||
</Text>
|
</Text>
|
||||||
|
<TouchableOpacity style={styles.viewButton} onPress={onViewPress}>
|
||||||
|
<Text style={styles.viewButtonText}>View</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
@@ -200,12 +204,29 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#F3F4F6',
|
||||||
|
paddingTop: 16,
|
||||||
},
|
},
|
||||||
dateText: {
|
dateText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: '#4B5563',
|
color: '#4B5563',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
},
|
},
|
||||||
|
// --- NEW STYLES for the View Button ---
|
||||||
|
viewButton: {
|
||||||
|
backgroundColor: colors.pastelOrange,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 10,
|
||||||
|
},
|
||||||
|
viewButtonText: {
|
||||||
|
color: '#000000ff',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const MyGroupCard: React.FC<Props> = ({ item, onManage }) => {
|
|||||||
{item.groupName}
|
{item.groupName}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}>
|
<View style={[styles.statusBadge, { backgroundColor: getStatusColor() }]}>
|
||||||
<Text style={styles.statusBadgeText}>{item.status === "ACTIVE" ? "Active" : "Not Active"}</Text>
|
<Text style={styles.statusBadgeText}>{item.status}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import Transaction from "../screens/profile/Transaction";
|
|||||||
import GroupDetails from "../screens/home/user-home/GroupDetails";
|
import GroupDetails from "../screens/home/user-home/GroupDetails";
|
||||||
import EditGroupScreen from "../screens/management/EditGroupScreen";
|
import EditGroupScreen from "../screens/management/EditGroupScreen";
|
||||||
import EditBankDetails from "../screens/profile/EditBankDetails";
|
import EditBankDetails from "../screens/profile/EditBankDetails";
|
||||||
|
import ManageJoinedGroup from "../screens/management/ManageJoinedGroup";
|
||||||
|
import InviteScreen from "../screens/InviteScreen";
|
||||||
|
import ManagementTabNavigation from "../screens/management/ManagementTabNavigation";
|
||||||
import TransactionsList from "../screens/transaction/TransactionList";
|
import TransactionsList from "../screens/transaction/TransactionList";
|
||||||
|
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
@@ -33,23 +36,39 @@ export type RootStackParamList = {
|
|||||||
YourContacts: undefined;
|
YourContacts: undefined;
|
||||||
Transaction: undefined;
|
Transaction: undefined;
|
||||||
EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null };
|
EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null };
|
||||||
TransactionList:undefined
|
ManageJoinedGroup: { groupId: string | number };
|
||||||
|
Invite: { groupId: string | number; code: string | number };
|
||||||
|
ManagementTabs:{ groupId: string | number;}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||||
|
|
||||||
export default function AppNavigator() {
|
export default function AppNavigator() {
|
||||||
const linking = {
|
const linking = {
|
||||||
prefixes: [
|
prefixes: [
|
||||||
"myapp://", // A custom scheme for internal or test links.
|
"myapp://", // Your custom scheme
|
||||||
"https://192.168.1.19" // Your production domain for verified App Links.
|
"https://your-production-domain.com" // Your website for Universal Links/App Links
|
||||||
],
|
],
|
||||||
config: {
|
config: {
|
||||||
screens: {
|
screens: {
|
||||||
Profile: "invite",
|
// Define the route for the InviteScreen
|
||||||
|
Invite: {
|
||||||
|
// This path matches the part after the "://" and before the "?"
|
||||||
|
path: "invite",
|
||||||
|
|
||||||
|
// This block tells React Navigation how to handle the query parameters
|
||||||
|
parse: {
|
||||||
|
groupId: (groupId: string | number) => `${groupId}`,
|
||||||
|
code: (code: string | number) => `${code}`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Other screens in your navigator can be defined here
|
||||||
|
// e.g., GroupDetails: "group/:id"
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationContainer linking={linking}>
|
<NavigationContainer linking={linking}>
|
||||||
@@ -68,6 +87,9 @@ export default function AppNavigator() {
|
|||||||
<Stack.Screen name="GroupDetails" component={GroupDetails} options={{ headerShown: false }} />
|
<Stack.Screen name="GroupDetails" component={GroupDetails} options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="EditGroup" component={EditGroupScreen} options={{ headerShown: false }} />
|
<Stack.Screen name="EditGroup" component={EditGroupScreen} options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="EditBankDetails" component={EditBankDetails} options={{ headerShown: false }} />
|
<Stack.Screen name="EditBankDetails" component={EditBankDetails} options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="ManageJoinedGroup" component={ManageJoinedGroup} options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="Invite" component={InviteScreen} options={{ headerShown: false }} />
|
||||||
|
<Stack.Screen name="ManagementTabs" component={ManagementTabNavigation} options={{ headerShown: false }} />
|
||||||
<Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} />
|
<Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
import { BottomTabNavigationProp, createBottomTabNavigator } from "@react-navigation/bottom-tabs";
|
||||||
import { View, Text, StyleSheet, Platform } from "react-native";
|
import { View, Text, StyleSheet, Platform } from "react-native";
|
||||||
import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons";
|
import { Ionicons, IoniconsIconName } from "@react-native-vector-icons/ionicons";
|
||||||
import GroupScreen from "../screens/GroupScreen";
|
import GroupScreen from "../screens/GroupScreen";
|
||||||
@@ -9,7 +9,6 @@ import colors from "../components/colors";
|
|||||||
import Setting from "../screens/setting/Setting";
|
import Setting from "../screens/setting/Setting";
|
||||||
import ManagementScreen from "../screens/management/ManagementScreen";
|
import ManagementScreen from "../screens/management/ManagementScreen";
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator();
|
|
||||||
|
|
||||||
const TAB_CONFIG = {
|
const TAB_CONFIG = {
|
||||||
activeColor: "#000",
|
activeColor: "#000",
|
||||||
@@ -29,26 +28,24 @@ const activeTabColors: { [key: string]: string } = {
|
|||||||
|
|
||||||
// Icon/Text colors on active tabs
|
// Icon/Text colors on active tabs
|
||||||
const activeIconColors: { [key: string]: string } = {
|
const activeIconColors: { [key: string]: string } = {
|
||||||
Home: "#333",
|
Home: "#333",
|
||||||
Profile: "#333",
|
Profile: "#333",
|
||||||
Group: "#333",
|
Group: "#333",
|
||||||
Management: "#333",
|
Management: "#333",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function HomeScreen() {
|
export type RootTabParamList = {
|
||||||
return (
|
Home: undefined;
|
||||||
<View style={styles.screen}><Text style={styles.text}>🏠 Home</Text></View>
|
Profile: undefined;
|
||||||
);
|
Group: undefined;
|
||||||
}
|
Management: { actionTab?: "My Groups" | "Joined Groups" | "Requests" | "Your Requests" };
|
||||||
|
};
|
||||||
|
// Define the navigation prop type for screens within the tab navigator
|
||||||
|
export type TabNavigationProp = BottomTabNavigationProp<RootTabParamList>;
|
||||||
|
const Tab = createBottomTabNavigator<RootTabParamList>();
|
||||||
|
|
||||||
function SettingsScreen() {
|
const BottomTabs: React.FC = () => {
|
||||||
return (
|
|
||||||
<View style={styles.screen}><Text style={styles.text}>⚙️ Settings</Text></View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function BottomTabs() {
|
|
||||||
return (
|
return (
|
||||||
<Tab.Navigator
|
<Tab.Navigator
|
||||||
screenOptions={({ route }) => ({
|
screenOptions={({ route }) => ({
|
||||||
@@ -151,3 +148,4 @@ const styles = StyleSheet.create({
|
|||||||
shadowRadius: 5,
|
shadowRadius: 5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
export default BottomTabs
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// navigation/types.ts
|
||||||
|
|
||||||
|
import { NavigatorScreenParams } from '@react-navigation/native';
|
||||||
|
import { StackScreenProps } from '@react-navigation/stack';
|
||||||
|
|
||||||
|
// Type for the parameters passed to each screen in the Bottom Tab Navigator
|
||||||
|
export type RootTabParamList = {
|
||||||
|
Home: undefined;
|
||||||
|
Profile: undefined;
|
||||||
|
Group: undefined;
|
||||||
|
Management: { actionTab?: "My Groups" | "Joined Groups" | "Requests" | "Your Requests" };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Type for the parameters passed to each screen in the main Stack Navigator
|
||||||
|
|
||||||
|
export type RootStackParamList = {
|
||||||
|
Splash: undefined;
|
||||||
|
Login: undefined;
|
||||||
|
OtpVarify: { mobile: string };
|
||||||
|
Home: undefined;
|
||||||
|
HomeTabs: undefined;
|
||||||
|
OrganizerRegister: undefined;
|
||||||
|
Profile: undefined;
|
||||||
|
GroupCreation: undefined;
|
||||||
|
GroupReview: undefined;
|
||||||
|
EditGroup: { groupId: string | number };
|
||||||
|
GroupDetails: { item:object };
|
||||||
|
BecomeOrg: undefined;
|
||||||
|
YourContacts: undefined;
|
||||||
|
Transaction: undefined;
|
||||||
|
EditBankDetails: { currentBankName: string | null; currentAccountNumber: string | null; currentBranchCode: string | null };
|
||||||
|
ManageJoinedGroup: { groupId: string | number };
|
||||||
|
Invite: { groupId: string | number; code: string | number };
|
||||||
|
ManagementTabs:any
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// Helper type for screens within the main stack navigator
|
||||||
|
export type AppStackScreenProps<T extends keyof RootStackParamList> = StackScreenProps<
|
||||||
|
RootStackParamList,
|
||||||
|
T
|
||||||
|
>;
|
||||||
@@ -39,9 +39,9 @@ export default function GroupCreationForm() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const cycleTypes = ["CUSTOM", "MONTHLY", "QUARTERLY", "YEARLY"];
|
const cycleTypes = ["CUSTOM", "MONTHLY", "QUARTERLY", "YEARLY", "HALF_YEARLY"];
|
||||||
const distributionTypes = ["SEQUENTIAL", "RANDOM"];
|
const distributionTypes = ["SEQUENTIAL", "RANDOM"];
|
||||||
const lateFeeModes = ["DAILY", "FIXED"];
|
const lateFeeModes = ["DAILY", "MONTHLY"];
|
||||||
|
|
||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors: any = {};
|
const newErrors: any = {};
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
|
import React, { useState, useEffect, useCallback, FC } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
ScrollView,
|
||||||
|
TouchableOpacity,
|
||||||
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
|
} from 'react-native';
|
||||||
|
import { RootStackParamList } from '../navigation/AppNavigator'; // Adjust path
|
||||||
|
import { apiCall } from '../utils/ApiHelper'; // Adjust path
|
||||||
|
import AppView from '../components/AppView'; // Adjust path
|
||||||
|
import colors from '../components/colors'; // Adjust path
|
||||||
|
|
||||||
|
// --- Type Definitions ---
|
||||||
|
interface Participant {
|
||||||
|
id: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string | null;
|
||||||
|
mobile: string;
|
||||||
|
role: 'ORGANIZER' | 'USER';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupDetails {
|
||||||
|
id: string;
|
||||||
|
groupName: string;
|
||||||
|
description: string;
|
||||||
|
contributionAmount: number;
|
||||||
|
maxParticipants: number;
|
||||||
|
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
|
||||||
|
status: 'PENDING' | 'ACTIVE' | 'COMPLETED' | 'APPROVED';
|
||||||
|
organizerFirstName: string;
|
||||||
|
organizerLastName: string | null;
|
||||||
|
totalParticipants: number;
|
||||||
|
participants: Participant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type InviteScreenProps = NativeStackScreenProps<RootStackParamList, 'Invite'>; // Assuming 'Invite' is in your stack
|
||||||
|
|
||||||
|
// --- Main Screen Component ---
|
||||||
|
const InviteScreen: FC<InviteScreenProps> = ({ route }) => {
|
||||||
|
const { groupId,code} = route.params;
|
||||||
|
|
||||||
|
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [joining, setJoining] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchGroupDetails = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
|
||||||
|
if (response.status === 'success' && response.data) {
|
||||||
|
setGroupDetails(response.data);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Failed to fetch group invitation details.');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [groupId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchGroupDetails();
|
||||||
|
}, [fetchGroupDetails]);
|
||||||
|
|
||||||
|
const handleJoinGroup = async () => {
|
||||||
|
if (!groupDetails) return;
|
||||||
|
|
||||||
|
setJoining(true);
|
||||||
|
try {
|
||||||
|
// Replace with your actual API call to join the group
|
||||||
|
// For now, it just shows an alert.
|
||||||
|
await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/groups/join/invite/${code}`, 'POST');
|
||||||
|
Alert.alert('Success!', `You have requested to join "${groupDetails.groupName}".`);
|
||||||
|
} catch (e: any) {
|
||||||
|
Alert.alert('Join Error', e.message || 'An unexpected error occurred.');
|
||||||
|
} finally {
|
||||||
|
setJoining(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color={colors.primary} /></View>;
|
||||||
|
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
|
||||||
|
if (!groupDetails) return null;
|
||||||
|
|
||||||
|
const organizerName = `${groupDetails.organizerFirstName} ${groupDetails.organizerLastName || ''}`.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppView>
|
||||||
|
<ScrollView contentContainerStyle={styles.contentContainer}>
|
||||||
|
<Text style={styles.headerTitle}>You're Invited!</Text>
|
||||||
|
<Text style={styles.headerSubtitle}>Join the group <Text style={{ fontWeight: 'bold' }}>{groupDetails.groupName}</Text></Text>
|
||||||
|
|
||||||
|
{/* Group Info Card */}
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Group Details </Text>
|
||||||
|
<InfoRow label="Organizer" value={organizerName} />
|
||||||
|
<InfoRow label="Contribution" value={`₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`} />
|
||||||
|
<InfoRow label="Payment Cycle" value={groupDetails.cycleType} />
|
||||||
|
<InfoRow label="Status" value={groupDetails.status} />
|
||||||
|
<InfoRow label="Code" value={code} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Participants Card */}
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants} / {groupDetails.maxParticipants})</Text>
|
||||||
|
{groupDetails.participants.length > 0 ? (
|
||||||
|
groupDetails.participants.map(p => (
|
||||||
|
<View key={p.id} style={styles.participantRow}>
|
||||||
|
<Text style={styles.participantName}>{`${p.firstName} ${p.lastName || ''}`.trim()}</Text>
|
||||||
|
{p.role === 'ORGANIZER' && <View style={styles.organizerBadge}><Text style={styles.organizerBadgeText}>Organizer</Text></View>}
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text style={styles.noParticipantsText}>Be the first to join!</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Floating Join Button */}
|
||||||
|
<View style={styles.floatingButtonContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.joinButton, groupDetails.status !== 'PENDING' && groupDetails.status !== 'APPROVED' ? styles.disabledButton : {}]}
|
||||||
|
onPress={handleJoinGroup}
|
||||||
|
disabled={joining || (groupDetails.status !== 'PENDING' && groupDetails.status !== 'APPROVED')}
|
||||||
|
>
|
||||||
|
{joining ? (
|
||||||
|
<ActivityIndicator color="#FFFFFF" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.joinButtonText}>
|
||||||
|
{groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED' ? 'Join Group' : `Group is ${groupDetails.status}`}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</AppView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper component for displaying rows
|
||||||
|
const InfoRow = ({ label, value }: { label: string; value: string | number }) => (
|
||||||
|
<View style={styles.infoRow}>
|
||||||
|
<Text style={styles.infoLabel}>{label}</Text>
|
||||||
|
<Text style={styles.infoValue}>{value}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
centered: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F3F4F6' },
|
||||||
|
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center', padding: 20 },
|
||||||
|
contentContainer: { padding: 16, paddingBottom: 100 }, // Add padding to avoid overlap with button
|
||||||
|
|
||||||
|
headerTitle: { fontSize: 32, fontWeight: 'bold', color: '#111827', textAlign: 'center' },
|
||||||
|
headerSubtitle: { fontSize: 18, color: '#4B5563', textAlign: 'center', marginBottom: 24, marginTop: 4 },
|
||||||
|
|
||||||
|
card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 },
|
||||||
|
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937', marginBottom: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10 },
|
||||||
|
|
||||||
|
infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 },
|
||||||
|
infoLabel: { fontSize: 16, color: '#6B7280' },
|
||||||
|
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
|
||||||
|
|
||||||
|
participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 10 },
|
||||||
|
participantName: { fontSize: 16, color: '#1F2937' },
|
||||||
|
organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12 },
|
||||||
|
organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' },
|
||||||
|
noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 10 },
|
||||||
|
|
||||||
|
floatingButtonContainer: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 20,
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
joinButton: {
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
borderRadius: 14,
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
elevation: 5,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 4,
|
||||||
|
width: '80%',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
joinButtonText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
disabledButton: {
|
||||||
|
backgroundColor: '#9CA3AF', // Gray color when disabled
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default InviteScreen;
|
||||||
|
|
||||||
+117
-81
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
@@ -8,88 +8,70 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
Image,
|
Image,
|
||||||
Alert,
|
Alert,
|
||||||
|
FlatList,
|
||||||
|
ListRenderItem, // Import ListRenderItem for better typing
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import AppView from "../../components/AppView";
|
import AppView from "../../components/AppView";
|
||||||
import UserProfileStatus from "./UserProfileStatus";
|
|
||||||
import colors from "../../components/colors";
|
import colors from "../../components/colors";
|
||||||
import GroupsTabs from "./GroupsTabs";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { RouteProp, useFocusEffect, useNavigation, useRoute } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import UserHome from "./user-home/UserHome";
|
|
||||||
import { PROFILE_URL } from "../../utils/ApiUrls";
|
import { PROFILE_URL } from "../../utils/ApiUrls";
|
||||||
import *as Keychain from "react-native-keychain"
|
import * as Keychain from "react-native-keychain";
|
||||||
import { setUserData } from "../../redux/userSlice";
|
import { setUserData } from "../../redux/userSlice";
|
||||||
// Enable animation for Android
|
import { TabNavigationProp } from "../../navigation/BottomTabs";
|
||||||
|
|
||||||
|
// Enable layout animation for Android
|
||||||
if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) {
|
if (Platform.OS === "android" && UIManager.setLayoutAnimationEnabledExperimental) {
|
||||||
UIManager.setLayoutAnimationEnabledExperimental(true);
|
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
type RootStackParamList = {
|
// --- TYPE DEFINITIONS ---
|
||||||
Home: { refreshProfile: boolean; role: string };
|
interface ActionItem {
|
||||||
};
|
|
||||||
|
|
||||||
type UserProfile = {
|
|
||||||
id: string;
|
id: string;
|
||||||
userId: number;
|
title: string;
|
||||||
firstName: string | null;
|
action: string;
|
||||||
lastName: string | null;
|
tab?: "My Groups" | "Joined Groups" | "Requests" | "Your Requests";
|
||||||
imageBlob: string | null;
|
imageUri: string;
|
||||||
role?: string;
|
|
||||||
}
|
}
|
||||||
const Home = () => {
|
|
||||||
const user = useSelector((state: any) => state.user);
|
|
||||||
const navigation = useNavigation();
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
|
|
||||||
const [apiUser, setApiUser] = useState<UserProfile | null | any>(null)
|
// Data for the grid items
|
||||||
|
const organizerActions: ActionItem[] = [
|
||||||
|
{ id: "1", title: "My Groups", action: "NAVIGATE_MANAGEMENT", tab: "My Groups", imageUri: "https://via.placeholder.com/60" },
|
||||||
|
{ id: "2", title: "Joined Groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", imageUri: "https://via.placeholder.com/60" },
|
||||||
|
{ id: "3", title: "Requests", action: "NAVIGATE_MANAGEMENT", tab: "Requests", imageUri: "https://via.placeholder.com/60" },
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
const userActions: ActionItem[] = [
|
||||||
|
{ id: "1", title: "Joined Groups", action: "NAVIGATE_MANAGEMENT", tab: "Joined Groups", imageUri: "https://via.placeholder.com/60" },
|
||||||
|
{ id: "2", title: "Your Requests", action: "NAVIGATE_MANAGEMENT", tab: "Your Requests", imageUri: "https://via.placeholder.com/60" },
|
||||||
|
{ id: "3", title: "Action 3", action: "ACTION_3", imageUri: "https://via.placeholder.com/60" },
|
||||||
|
{ id: "4", title: "Action 4", action: "ACTION_4", imageUri: "https://via.placeholder.com/60" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const Home: React.FC = () => {
|
||||||
|
const user = useSelector((state: any) => state.user);
|
||||||
|
const navigation = useNavigation<TabNavigationProp>();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
// --- FIXED: Conditional logic moved inside the component ---
|
||||||
|
const actions = user.role === 'ORGANIZER' ? organizerActions : userActions;
|
||||||
|
|
||||||
const fetchUserProfile = async () => {
|
const fetchUserProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const credentials = await Keychain.getGenericPassword();
|
const credentials = await Keychain.getGenericPassword();
|
||||||
if (!credentials) {
|
if (!credentials) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
const token = credentials.password;
|
const token = credentials.password;
|
||||||
|
|
||||||
console.log("token : ", token)
|
const response = await fetch(PROFILE_URL, {
|
||||||
const response = await fetch(
|
method: "GET",
|
||||||
PROFILE_URL,
|
headers: { Accept: "*/*", Authorization: `Bearer ${token}` },
|
||||||
{
|
});
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Accept: "*/*",
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log("👤 Profile Response:", result);
|
|
||||||
if (result.status === "success") {
|
if (result.status === "success") {
|
||||||
setApiUser(result.data);
|
// No need for apiUser state, just dispatch to Redux
|
||||||
dispatch(
|
dispatch(setUserData({ ...result.data, profileStatus: true }));
|
||||||
setUserData({
|
|
||||||
id: result.data.id,
|
|
||||||
userId: result.data.userId,
|
|
||||||
username: result.data.username,
|
|
||||||
firstName: result.data.firstName,
|
|
||||||
lastName: result.data.lastName,
|
|
||||||
gender: result.data.gender,
|
|
||||||
mobile: result.data.mobile,
|
|
||||||
email: result.data.email,
|
|
||||||
address: result.data.address,
|
|
||||||
bankAccountNumber: result.data.bankAccountNumber,
|
|
||||||
bankName: result.data.bankName,
|
|
||||||
branchCode: result.data.branchCode,
|
|
||||||
profilePicture: result.data.filePath,
|
|
||||||
filePath: result.data.filePath,
|
|
||||||
imageBlob: result.data.imageBlob,
|
|
||||||
role: result.data.role,
|
|
||||||
organizerId: result.data.organizerId,
|
|
||||||
organizerStatus: result.data.organizerStatus,
|
|
||||||
profileStatus: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Error fetching profile:", error);
|
console.error("❌ Error fetching profile:", error);
|
||||||
@@ -100,26 +82,45 @@ const Home = () => {
|
|||||||
fetchUserProfile();
|
fetchUserProfile();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// --- REFACTORED: Simplified action handler ---
|
||||||
|
const handleActionPress = (item: ActionItem) => {
|
||||||
|
if (item.action === "NAVIGATE_MANAGEMENT" && item.tab) {
|
||||||
|
navigation.navigate('Management', { actionTab: item.tab });
|
||||||
|
} else if (item.action === "ACTION_3") {
|
||||||
|
Alert.alert("Action 3 pressed");
|
||||||
|
} else if (item.action === "ACTION_4") {
|
||||||
|
Alert.alert("Action 4 pressed");
|
||||||
|
} else {
|
||||||
|
Alert.alert("Unknown action", item.title);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render function for each grid item
|
||||||
|
const renderGridItem: ListRenderItem<ActionItem> = ({ item }) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.gridButton}
|
||||||
|
onPress={() => handleActionPress(item)}
|
||||||
|
>
|
||||||
|
{/* FIXED: 'imageUri' was missing from your data objects */}
|
||||||
|
<Image source={{ uri: item.imageUri }} style={styles.gridIcon} />
|
||||||
|
<Text style={styles.gridLabel}>{item.title}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppView barStyle="dark-content" style={styles.container}>
|
<AppView barStyle="dark-content" style={styles.container}>
|
||||||
{/* Header */}
|
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.greeting}>Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}👋</Text>
|
<Text style={styles.headerTitle}>Home</Text>
|
||||||
<TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}>
|
|
||||||
<Image
|
|
||||||
source={
|
|
||||||
user.imageBlob
|
|
||||||
&& { uri: user.imageBlob || apiUser?.imageBlob ? `data:image/jpeg;base64,${user.imageBlob || apiUser?.imageBlob}` : user.profilePicture || apiUser?.imageBlob }
|
|
||||||
}
|
|
||||||
style={{ width: 40, height: 40, borderRadius: 20, borderColor: colors.primary, borderWidth: 2 }}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
</View>
|
||||||
{/* Status */}
|
|
||||||
<UserProfileStatus />
|
<FlatList
|
||||||
{/* <UserHome /> */}
|
data={actions}
|
||||||
{user?.role === "USER" || apiUser?.role === "USER" ? <UserHome /> :
|
renderItem={renderGridItem}
|
||||||
<GroupsTabs />}
|
keyExtractor={(item) => item.id}
|
||||||
|
numColumns={2}
|
||||||
|
columnWrapperStyle={styles.row}
|
||||||
|
contentContainerStyle={{ paddingHorizontal: 8 }}
|
||||||
|
/>
|
||||||
</AppView>
|
</AppView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -128,13 +129,48 @@ export default Home;
|
|||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
paddingBottom: '25%',
|
flex: 1,
|
||||||
|
backgroundColor: '#F4F6F8'
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: "row",
|
justifyContent: "center",
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
paddingVertical: 12,
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: "600",
|
||||||
|
color: colors.Home,
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "space-around",
|
||||||
|
},
|
||||||
|
gridButton: {
|
||||||
|
flex: 1,
|
||||||
|
margin: 8,
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 16,
|
||||||
|
alignItems: "center",
|
||||||
|
shadowColor: "#000",
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 5,
|
||||||
|
elevation: 4,
|
||||||
|
},
|
||||||
|
gridIcon: {
|
||||||
|
width: 60,
|
||||||
|
height: 60,
|
||||||
marginBottom: 12,
|
marginBottom: 12,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: '#E0E0E0',
|
||||||
|
},
|
||||||
|
gridLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: "500",
|
||||||
|
color: colors.Home,
|
||||||
|
textAlign: "center",
|
||||||
},
|
},
|
||||||
greeting: { fontSize: 22, fontWeight: "600", color: colors.Home },
|
|
||||||
});
|
});
|
||||||
@@ -9,11 +9,17 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Alert,
|
Alert,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Share,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
|
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
|
||||||
import { apiCall } from '../../utils/ApiHelper'; // Adjust path
|
import { apiCall } from '../../utils/ApiHelper'; // Adjust path
|
||||||
import AppView from '../../components/AppView';
|
import AppView from '../../components/AppView';
|
||||||
import { Picker } from '@react-native-picker/picker';
|
import { Picker } from '@react-native-picker/picker';
|
||||||
|
import colors from '../../components/colors';
|
||||||
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
|
import { CompositeScreenProps } from '@react-navigation/native';
|
||||||
|
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
|
||||||
|
import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path
|
||||||
|
|
||||||
// --- Type Definitions ---
|
// --- Type Definitions ---
|
||||||
interface Participant {
|
interface Participant {
|
||||||
@@ -35,6 +41,8 @@ export interface GroupDetails {
|
|||||||
maxParticipants: number;
|
maxParticipants: number;
|
||||||
portalCharge: number | null;
|
portalCharge: number | null;
|
||||||
latePaymentCharge: number | null;
|
latePaymentCharge: number | null;
|
||||||
|
lateFeeMode: 'DAILY' | 'FIXED' | null;
|
||||||
|
lateFeeValue: number | null;
|
||||||
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
|
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
|
||||||
customCycleDays: number;
|
customCycleDays: number;
|
||||||
withdrawalMethod: 'RANDOM' | 'SEQUENTIAL';
|
withdrawalMethod: 'RANDOM' | 'SEQUENTIAL';
|
||||||
@@ -54,16 +62,23 @@ export interface EditGroupPayload {
|
|||||||
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
|
cycleType: 'QUARTERLY' | 'MONTHLY' | 'WEEKLY' | 'CUSTOM';
|
||||||
latePaymentCharge: number;
|
latePaymentCharge: number;
|
||||||
visibility: 'PUBLIC' | 'PRIVATE';
|
visibility: 'PUBLIC' | 'PRIVATE';
|
||||||
|
lateFeeMode: 'DAILY' | 'FIXED' | null;
|
||||||
|
lateFeeValue: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EditGroupScreenProps = NativeStackScreenProps<RootStackParamList, 'EditGroup'>;
|
// Correct props for a screen inside a nested Tab Navigator
|
||||||
type EditingField = keyof EditGroupPayload | null;
|
type EditGroupScreenProps = CompositeScreenProps<
|
||||||
|
BottomTabScreenProps<ManagementTabParamList, 'EditGroup'>,
|
||||||
|
NativeStackScreenProps<RootStackParamList>
|
||||||
|
>;
|
||||||
|
|
||||||
|
type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null;
|
||||||
|
|
||||||
// --- Main Screen Component ---
|
// --- Main Screen Component ---
|
||||||
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||||
const { groupId } = route.params;
|
const { groupId } = route.params;
|
||||||
|
|
||||||
const [editingField, setEditingField] = useState<EditingField>(null);
|
const [editingCard, setEditingCard] = useState<EditingCard>(null);
|
||||||
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
|
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
|
||||||
const [editableData, setEditableData] = useState<EditGroupPayload>({
|
const [editableData, setEditableData] = useState<EditGroupPayload>({
|
||||||
groupName: '',
|
groupName: '',
|
||||||
@@ -74,30 +89,41 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
cycleType: 'MONTHLY',
|
cycleType: 'MONTHLY',
|
||||||
latePaymentCharge: 0,
|
latePaymentCharge: 0,
|
||||||
visibility: 'PRIVATE',
|
visibility: 'PRIVATE',
|
||||||
|
lateFeeMode: null,
|
||||||
|
lateFeeValue: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [starting, setStarting] = useState(false);
|
const [starting, setStarting] = useState(false);
|
||||||
|
const [inviting, setInviting] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// ... (All your handler functions: initializeEditableData, fetchGroupDetails, handleCancel, handleSave, etc. remain the same)
|
||||||
|
const initializeEditableData = (data: GroupDetails) => {
|
||||||
|
setEditableData({
|
||||||
|
groupName: data.groupName,
|
||||||
|
description: data.description,
|
||||||
|
maxParticipants: data.maxParticipants,
|
||||||
|
contributionAmount: data.contributionAmount,
|
||||||
|
customCycleDays: data.customCycleDays,
|
||||||
|
cycleType: data.cycleType,
|
||||||
|
latePaymentCharge: data.latePaymentCharge ?? 0,
|
||||||
|
visibility: data.visibility,
|
||||||
|
lateFeeMode: data.lateFeeMode ?? null,
|
||||||
|
lateFeeValue: data.lateFeeValue ?? null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const fetchGroupDetails = useCallback(async () => {
|
const fetchGroupDetails = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
|
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/details?groupId=${groupId}`, 'GET') as { status: string; data?: GroupDetails; message?: string };
|
||||||
|
console.log("groupDetails",response)
|
||||||
if (response.status === 'success' && response.data) {
|
if (response.status === 'success' && response.data) {
|
||||||
setGroupDetails(response.data);
|
setGroupDetails(response.data);
|
||||||
setEditableData({
|
initializeEditableData(response.data);
|
||||||
groupName: response.data.groupName,
|
|
||||||
description: response.data.description,
|
|
||||||
maxParticipants: response.data.maxParticipants,
|
|
||||||
contributionAmount: response.data.contributionAmount,
|
|
||||||
customCycleDays: response.data.customCycleDays,
|
|
||||||
cycleType: response.data.cycleType,
|
|
||||||
latePaymentCharge: response.data.latePaymentCharge ?? 0,
|
|
||||||
visibility: response.data.visibility,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.message || 'Failed to fetch group data.');
|
throw new Error(response.message || 'Failed to fetch group data.');
|
||||||
}
|
}
|
||||||
@@ -114,36 +140,24 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
if (groupDetails) {
|
if (groupDetails) {
|
||||||
setEditableData({
|
initializeEditableData(groupDetails);
|
||||||
groupName: groupDetails.groupName,
|
|
||||||
description: groupDetails.description,
|
|
||||||
maxParticipants: groupDetails.maxParticipants,
|
|
||||||
contributionAmount: groupDetails.contributionAmount,
|
|
||||||
customCycleDays: groupDetails.customCycleDays,
|
|
||||||
cycleType: groupDetails.cycleType,
|
|
||||||
latePaymentCharge: groupDetails.latePaymentCharge ?? 0,
|
|
||||||
visibility: groupDetails.visibility,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setEditingField(null);
|
setEditingCard(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!groupDetails || !editingField) return;
|
if (!groupDetails) return;
|
||||||
|
|
||||||
if (editingField === 'maxParticipants') {
|
if (editableData.maxParticipants < groupDetails.totalParticipants) {
|
||||||
const newMaxParticipants = Number(editableData.maxParticipants);
|
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
|
||||||
if (newMaxParticipants < groupDetails.totalParticipants) {
|
return;
|
||||||
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/apifund/groups/${groupDetails.id}/edit`, 'PUT', editableData);
|
await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/groups/${groupDetails.id}/edit`, 'PUT', editableData);
|
||||||
Alert.alert('Success', 'Group has been updated.');
|
Alert.alert('Success', 'Group has been updated.');
|
||||||
setEditingField(null);
|
setEditingCard(null);
|
||||||
await fetchGroupDetails();
|
await fetchGroupDetails();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
Alert.alert('Save Error', e.message || 'An unexpected error occurred.');
|
Alert.alert('Save Error', e.message || 'An unexpected error occurred.');
|
||||||
@@ -180,123 +194,173 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInvite = async () => {
|
||||||
|
if (!groupDetails) return;
|
||||||
|
setInviting(true);
|
||||||
|
try {
|
||||||
|
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
|
||||||
|
|
||||||
|
const inviteLink = response.data;
|
||||||
|
|
||||||
|
if (inviteLink) {
|
||||||
|
Clipboard.setString(inviteLink);
|
||||||
|
Alert.alert('Copied!', 'Invitation link copied to clipboard.');
|
||||||
|
await Share.share({
|
||||||
|
message: `Join my group on CircleCollect! ${inviteLink}`,
|
||||||
|
url: inviteLink,
|
||||||
|
title: 'CircleCollect Group Invitation'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not generate invitation link.');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
Alert.alert('Error', error.message || 'Failed to generate invitation.');
|
||||||
|
} finally {
|
||||||
|
setInviting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
|
const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
|
||||||
setEditableData(prev => ({ ...prev, [field]: value }));
|
setEditableData(prev => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ... (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 <TextInput {...commonInputProps} value={editableData.groupName} onChangeText={(text) => handleUpdateField('groupName', text)} />;
|
||||||
|
case 'description':
|
||||||
|
return <TextInput {...commonInputProps} style={[styles.input, styles.textArea]} value={editableData.description} onChangeText={(text) => handleUpdateField('description', text)} multiline />;
|
||||||
|
case 'contributionAmount':
|
||||||
|
return <TextInput {...commonInputProps} value={String(editableData.contributionAmount ?? '')} onChangeText={(text) => handleUpdateField('contributionAmount', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
|
||||||
|
case 'maxParticipants':
|
||||||
|
return <TextInput {...commonInputProps} value={String(editableData.maxParticipants ?? '')} onChangeText={(text) => handleUpdateField('maxParticipants', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
|
||||||
|
case 'latePaymentCharge':
|
||||||
|
return <TextInput {...commonInputProps} value={String(editableData.latePaymentCharge ?? '')} onChangeText={(text) => handleUpdateField('latePaymentCharge', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
|
||||||
|
case 'cycleType':
|
||||||
|
return (
|
||||||
|
<Picker selectedValue={editableData.cycleType} onValueChange={(itemValue) => handleUpdateField('cycleType', itemValue)} style={styles.picker}>
|
||||||
|
{['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => <Picker.Item key={c} label={c} value={c} />)}
|
||||||
|
</Picker>
|
||||||
|
);
|
||||||
|
case 'customCycleDays':
|
||||||
|
if (editableData.cycleType !== 'CUSTOM') return null;
|
||||||
|
return <TextInput {...commonInputProps} value={String(editableData.customCycleDays ?? '')} onChangeText={(text) => handleUpdateField('customCycleDays', text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
|
||||||
|
case 'visibility':
|
||||||
|
return (
|
||||||
|
<Picker selectedValue={editableData.visibility} onValueChange={(itemValue) => handleUpdateField('visibility', itemValue)} style={styles.picker}>
|
||||||
|
<Picker.Item label="PUBLIC" value="PUBLIC" />
|
||||||
|
<Picker.Item label="PRIVATE" value="PRIVATE" />
|
||||||
|
</Picker>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.fieldContainer}>
|
||||||
|
<Text style={styles.infoLabel}>{label}</Text>
|
||||||
|
{isEditing && isEditable ? (
|
||||||
|
<View style={{marginTop: 8}}>{getInputForField()}</View>
|
||||||
|
) : (
|
||||||
|
<Text style={styles.infoValue}>{displayValue}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => {
|
||||||
|
const isEditingThisCard = editingCard === cardKey;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.card}>
|
||||||
|
<View style={styles.cardHeader}>
|
||||||
|
<Text style={styles.cardTitle}>{title}</Text>
|
||||||
|
{isEditable && !editingCard && (
|
||||||
|
<TouchableOpacity onPress={() => setEditingCard(cardKey)} style={styles.editButton}>
|
||||||
|
<Text style={styles.editButtonText}>✎</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{fields.map(({ field, label, displayValue }) =>
|
||||||
|
renderField(field, label, displayValue(), isEditingThisCard)
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isEditingThisCard && (
|
||||||
|
<View style={styles.cardActions}>
|
||||||
|
<TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={handleCancel} disabled={saving}>
|
||||||
|
<Text style={[styles.buttonText, { color: '#374151' }]}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity style={[styles.button, styles.saveButton]} onPress={handleSave} disabled={saving}>
|
||||||
|
{saving ? <ActivityIndicator color="#FFFFFF" size="small" /> : <Text style={styles.buttonText}>Save</Text>}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>;
|
if (loading) return <View style={styles.centered}><ActivityIndicator size="large" color="#4F46E5" /></View>;
|
||||||
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
|
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
|
||||||
if (!groupDetails) return null;
|
if (!groupDetails) return null;
|
||||||
|
|
||||||
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
|
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
|
||||||
|
const isInvitable = groupDetails.status === 'APPROVED';
|
||||||
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
|
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 <TextInput {...commonProps} value={editableData.groupName} onChangeText={(text) => handleUpdateField('groupName', text)} />;
|
|
||||||
case 'description':
|
|
||||||
return <TextInput {...commonProps} style={[styles.input, styles.textArea]} value={editableData.description} onChangeText={(text) => handleUpdateField('description', text)} multiline />;
|
|
||||||
case 'contributionAmount':
|
|
||||||
case 'maxParticipants':
|
|
||||||
case 'latePaymentCharge':
|
|
||||||
case 'customCycleDays':
|
|
||||||
return <TextInput {...commonProps} value={String(editableData[field] ?? '')} onChangeText={(text) => handleUpdateField(field, text ? Number(text.replace(/[^0-9]/g, '')) : 0)} keyboardType="numeric" />;
|
|
||||||
case 'cycleType':
|
|
||||||
return <Picker selectedValue={editableData.cycleType} onValueChange={(itemValue) => handleUpdateField('cycleType', itemValue)} style={styles.picker}>
|
|
||||||
{['MONTHLY', 'WEEKLY', 'QUARTERLY', 'CUSTOM'].map(c => <Picker.Item key={c} label={c} value={c} />)}
|
|
||||||
</Picker>;
|
|
||||||
case 'visibility':
|
|
||||||
return <Picker selectedValue={editableData.visibility} onValueChange={(itemValue) => handleUpdateField('visibility', itemValue)} style={styles.picker}>
|
|
||||||
<Picker.Item label="PUBLIC" value="PUBLIC" />
|
|
||||||
<Picker.Item label="PRIVATE" value="PRIVATE" />
|
|
||||||
</Picker>;
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderRow = (field: EditingField, label: string, displayValue: string | number) => {
|
|
||||||
return (
|
|
||||||
<View style={styles.fieldContainer}>
|
|
||||||
<View style={styles.infoRow}>
|
|
||||||
<Text style={styles.infoLabel}>{label}</Text>
|
|
||||||
{editingField !== field && (
|
|
||||||
<View style={styles.valueContainer}>
|
|
||||||
<Text style={styles.infoValue}>{displayValue}</Text>
|
|
||||||
{isEditable && <TouchableOpacity onPress={() => setEditingField(field)} style={styles.editButton}>
|
|
||||||
<Text style={styles.editButtonText}>✎</Text>
|
|
||||||
</TouchableOpacity>}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
{editingField === field && isEditable && (
|
|
||||||
<View style={styles.editingContainer}>
|
|
||||||
{renderInputField(field)}
|
|
||||||
<View style={styles.cardActions}>
|
|
||||||
<TouchableOpacity style={[styles.button, styles.cancelButton]} onPress={handleCancel} disabled={saving}>
|
|
||||||
<Text style={[styles.buttonText, { color: '#374151' }]}>Cancel</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity style={[styles.button, styles.saveButton]} onPress={handleSave} disabled={saving}>
|
|
||||||
{saving ? <ActivityIndicator color="#FFFFFF" size="small" /> : <Text style={styles.buttonText}>Save</Text>}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppView paddingHorizontal={0}>
|
// Use AppView as the root container with flex: 1
|
||||||
<ScrollView style={styles.container} contentContainerStyle={styles.contentContainer}>
|
<AppView style={{ flex: 1, paddingHorizontal: 0 }}>
|
||||||
<Text style={styles.headerTitle}>Edit Group</Text>
|
<View style={styles.headerContainer}>
|
||||||
|
<Text style={styles.headerTitle}>Manage Group</Text>
|
||||||
{canStartGroup && (
|
{isInvitable && (
|
||||||
<View style={styles.card}>
|
<TouchableOpacity style={[styles.button, styles.inviteButton]} onPress={handleInvite} disabled={inviting}>
|
||||||
<Text style={styles.cardTitle}>Ready to Start?</Text>
|
{inviting ? <ActivityIndicator color="#FFFFFF" /> : <Text style={styles.buttonText}>Invite</Text>}
|
||||||
<Text style={styles.startGroupText}>This group has met the minimum participant requirement.</Text>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity style={[styles.button, styles.startButton]} onPress={handleStartGroup} disabled={starting}>
|
|
||||||
{starting ? <ActivityIndicator color="#FFFFFF" /> : <Text style={styles.buttonText}>Start Group</Text>}
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)}
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={styles.card}>
|
<ScrollView
|
||||||
<Text style={styles.cardTitle}>Group Info</Text>
|
style={styles.container}
|
||||||
{renderRow('groupName', 'Group Name', groupDetails.groupName)}
|
contentContainerStyle={styles.contentContainer}
|
||||||
{renderRow('description', 'Description', groupDetails.description)}
|
showsVerticalScrollIndicator={false}
|
||||||
</View>
|
>
|
||||||
|
{renderCard('Group Info', [
|
||||||
|
{ field: 'groupName', label: 'Group Name', displayValue: () => groupDetails.groupName },
|
||||||
|
{ field: 'description', label: 'Description', displayValue: () => groupDetails.description },
|
||||||
|
], 'groupInfo')}
|
||||||
|
|
||||||
<View style={styles.card}>
|
{renderCard('Financial Rules', [
|
||||||
<Text style={styles.cardTitle}>Financial Rules</Text>
|
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
|
||||||
{renderRow('contributionAmount', 'Contribution', `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}`)}
|
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
|
||||||
{renderRow('maxParticipants', 'Max Participants', `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}`)}
|
{ field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
|
||||||
{renderRow('latePaymentCharge', 'Late Fee', `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}`)}
|
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
|
||||||
</View>
|
], 'financialRules')}
|
||||||
|
|
||||||
<View style={styles.card}>
|
{renderCard('Configuration', [
|
||||||
<Text style={styles.cardTitle}>Configuration</Text>
|
{ field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType },
|
||||||
{renderRow('cycleType', 'Payment Cycle', groupDetails.cycleType)}
|
{ field: 'customCycleDays', label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays ?? ''} days` },
|
||||||
{(editableData.cycleType === 'CUSTOM' || groupDetails.cycleType === 'CUSTOM') && renderRow('customCycleDays', 'Custom Days', `${editableData.customCycleDays ?? groupDetails.customCycleDays} days`)}
|
{ field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility },
|
||||||
{renderRow('visibility', 'Visibility', groupDetails.visibility)}
|
], 'configuration')}
|
||||||
</View>
|
|
||||||
|
|
||||||
|
{/* Participants Card ... */}
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
|
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
|
||||||
{groupDetails.participants.length > 0 ? (
|
{groupDetails.participants.length > 0 ? (
|
||||||
groupDetails.participants.map(p => (
|
groupDetails.participants.map(p => (
|
||||||
<View key={p.id} style={styles.participantRow}>
|
<View key={p.id} style={styles.participantRow}>
|
||||||
<View>
|
<View>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||||
<Text style={styles.participantName}>{`${p.firstName} ${p.lastName || ''}`.trim()}</Text>
|
<Text style={styles.participantName}>{`${p.firstName} ${p.lastName || ''}`.trim()}</Text>
|
||||||
{p.role === 'ORGANIZER' && <View style={styles.organizerBadge}><Text style={styles.organizerBadgeText}>Organizer</Text></View>}
|
{p.role === 'ORGANIZER' && <View style={styles.organizerBadge}><Text style={styles.organizerBadgeText}>Organizer</Text></View>}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.participantMobile}>{p.mobile}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.participantMobile}>{p.mobile}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
@@ -304,48 +368,103 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* --- FLOATING BUTTON AREA --- */}
|
||||||
|
{/* This View is a sibling of ScrollView, allowing it to float on top */}
|
||||||
|
{canStartGroup && (
|
||||||
|
<View style={styles.floatingButtonContainer}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.activateButton}
|
||||||
|
onPress={handleStartGroup}
|
||||||
|
disabled={starting}
|
||||||
|
>
|
||||||
|
{starting ? (
|
||||||
|
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.activateButtonText}>Activate Group</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</AppView>
|
</AppView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Styles ---
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: '#F3F4F6' },
|
container: { flex: 1, backgroundColor: '#F3F4F6' },
|
||||||
contentContainer: { padding: 16, paddingBottom: 40 },
|
// Added more paddingBottom to ensure content doesn't hide behind the button AND the tab bar
|
||||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 },
|
||||||
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
|
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||||
headerTitle: { fontSize: 28, fontWeight: 'bold', color: '#111827', marginBottom: 20 },
|
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
|
||||||
|
headerContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingTop: 16,
|
||||||
|
paddingBottom: 8,
|
||||||
|
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: '#ffffffff', fontSize: 15, fontWeight: '600' },
|
||||||
|
|
||||||
card: { backgroundColor: '#FFFFFF', borderRadius: 16, padding: 20, marginBottom: 16, shadowColor: '#374151', shadowOpacity: 0.05, shadowRadius: 10 },
|
// --- FLOATING BUTTON STYLES ---
|
||||||
cardTitle: { fontSize: 18, fontWeight: 'bold', color: '#1F2937', marginBottom: 12, borderBottomWidth: 1, borderBottomColor: '#E5E7EB', paddingBottom: 10 },
|
floatingButtonContainer: {
|
||||||
|
position: 'absolute',
|
||||||
editButton: { padding: 8, marginLeft: 8 },
|
bottom: 85, // Positioned above the tab bar
|
||||||
editButtonText: { color: '#4F46E5', fontSize: 20, fontWeight: 'bold' },
|
left: 20,
|
||||||
|
right: 20,
|
||||||
fieldContainer: { borderBottomWidth: 1, borderBottomColor: '#F3F4F6', paddingVertical: 12 },
|
alignItems: 'center', // Center the button horizontally
|
||||||
infoRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
zIndex: 1, // Ensure it's on top
|
||||||
infoLabel: { fontSize: 16, color: '#6B7280' },
|
},
|
||||||
infoValue: { fontSize: 16, fontWeight: '600', color: '#111827', textTransform: 'capitalize' },
|
activateButton: {
|
||||||
valueContainer: { flexDirection: 'row', alignItems: 'center' },
|
backgroundColor: colors.pastelOrange,
|
||||||
|
borderRadius: 14,
|
||||||
editingContainer: { marginTop: 12 },
|
paddingVertical: 16,
|
||||||
input: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 14, fontSize: 16, color: '#111827' },
|
paddingHorizontal: 32,
|
||||||
textArea: { height: 100, textAlignVertical: 'top' },
|
elevation: 8,
|
||||||
picker: { backgroundColor: '#F9FAFB', borderWidth: 1, borderColor: '#D1D5DB', borderRadius: 10, padding: 0, margin:0 },
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
participantRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#F3F4F6' },
|
shadowOpacity: 0.3,
|
||||||
participantName: { fontSize: 16, fontWeight: '600', color: '#111827' },
|
shadowRadius: 4,
|
||||||
participantMobile: { fontSize: 14, color: '#6B7280', marginTop: 4 },
|
width: '90%', // Adjust width as needed
|
||||||
organizerBadge: { backgroundColor: '#FEF3C7', paddingHorizontal: 8, paddingVertical: 3, borderRadius: 12, marginLeft: 10 },
|
alignItems: 'center',
|
||||||
organizerBadgeText: { color: '#92400E', fontSize: 12, fontWeight: '500' },
|
},
|
||||||
noParticipantsText: { fontSize: 15, color: '#6B7280', textAlign: 'center', paddingVertical: 20 },
|
activateButtonText: {
|
||||||
|
color: '#000000ff',
|
||||||
cardActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 20, paddingTop: 16, borderTopWidth: 1, borderTopColor: '#E5E7EB' },
|
fontSize: 18,
|
||||||
button: { borderRadius: 10, paddingVertical: 12, paddingHorizontal: 20, marginLeft: 10, minWidth: 80, alignItems: 'center', justifyContent: 'center' },
|
fontWeight: 'bold',
|
||||||
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' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default EditGroupScreen;
|
export default EditGroupScreen;
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { StyleSheet, Text, View } from 'react-native'
|
||||||
|
import React from 'react'
|
||||||
|
import { useRoute } from '@react-navigation/native';
|
||||||
|
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
|
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||||
|
type ManageJoinedGroupProps = NativeStackScreenProps<RootStackParamList, 'ManageJoinedGroup'>;
|
||||||
|
export default function ManageJoinedGroup({ route }: ManageJoinedGroupProps) {
|
||||||
|
|
||||||
|
const { groupId } = route.params as { groupId: string | number };
|
||||||
|
console.log("first", groupId)
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<Text>Manage Group: {groupId}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({})
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// src/screens/ManagementScreen.tsx
|
// src/screens/ManagementScreen.tsx
|
||||||
|
|
||||||
import React, { useState, useCallback, FC } from "react";
|
import React, { useState, useCallback, FC, useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -9,69 +9,75 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
ListRenderItemInfo,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useNavigation, useFocusEffect } from "@react-navigation/native";
|
import { useNavigation, useFocusEffect, useRoute, RouteProp } from "@react-navigation/native";
|
||||||
import { StackNavigationProp } from "@react-navigation/stack";
|
import { StackNavigationProp } from "@react-navigation/stack";
|
||||||
import { RootStackParamList } from "../../navigation/AppNavigator";
|
import { APPROVE_REQUEST_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, MY_GROUPS_URL, REQUESTS_URL, USER_REQUEST } from "../../utils/ApiUrls";
|
||||||
import { APPROVE_REQUEST_URL, BASE_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, JOIN_GROUP_URL, MY_GROUPS_URL, REQUESTS_URL, USER_REQUEST } from "../../utils/ApiUrls";
|
|
||||||
import colors from "../../components/colors";
|
import colors from "../../components/colors";
|
||||||
import AppView from "../../components/AppView";
|
import AppView from "../../components/AppView";
|
||||||
import { apiCall } from "../../utils/ApiHelper";
|
import { apiCall } from "../../utils/ApiHelper";
|
||||||
import * as Keychain from "react-native-keychain";
|
import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types'; // Adjust import path
|
||||||
import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types';
|
|
||||||
import { MyGroupCard } from "../../components/MyGroupCard";
|
import { MyGroupCard } from "../../components/MyGroupCard";
|
||||||
import { JoinedGroupCard } from "../../components/JoinedGroupCard";
|
import { JoinedGroupCard } from "../../components/JoinedGroupCard";
|
||||||
import { RequestCard } from "../../components/RequestCard";
|
import { RequestCard } from "../../components/RequestCard";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import Ionicons from "@react-native-vector-icons/ionicons";
|
|
||||||
import { UserRequested } from "../../components/UserRequested";
|
import { UserRequested } from "../../components/UserRequested";
|
||||||
|
import { RootTabParamList } from "../../navigation/BottomTabs"; // Adjust import path
|
||||||
|
import { RootStackParamList } from "../../navigation/AppNavigator";
|
||||||
|
|
||||||
type GroupManagementNavProp = StackNavigationProp<RootStackParamList>;
|
// --- TYPE DEFINITIONS ---
|
||||||
|
type GroupManagementNavProp = StackNavigationProp<RootStackParamList, 'ManagementTabs'>;
|
||||||
|
type ManagementRouteProp = RouteProp<RootTabParamList, 'Management'>;
|
||||||
type ActiveTab = "My Groups" | "Joined Groups" | "Requests" | "Your Requests";
|
type ActiveTab = "My Groups" | "Joined Groups" | "Requests" | "Your Requests";
|
||||||
|
|
||||||
const ManagementScreen: FC = () => {
|
const ManagementScreen: FC = () => {
|
||||||
const userRole = useSelector((state: any) => state.user)
|
const user = useSelector((state: any) => state.user);
|
||||||
const [activeTab, setActiveTab] = useState<ActiveTab>(userRole.role === "USER" ? "Joined Groups" : "My Groups");
|
|
||||||
const navigation = useNavigation<GroupManagementNavProp>();
|
const navigation = useNavigation<GroupManagementNavProp>();
|
||||||
|
const route = useRoute<ManagementRouteProp>();
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState<ActiveTab>(user.role === "USER" ? "Joined Groups" : "My Groups");
|
||||||
const [myGroups, setMyGroups] = useState<MyGroup[]>([]);
|
const [myGroups, setMyGroups] = useState<MyGroup[]>([]);
|
||||||
const [joinedGroups, setJoinedGroups] = useState<JoinedGroup[]>([]);
|
const [joinedGroups, setJoinedGroups] = useState<JoinedGroup[]>([]);
|
||||||
const [requests, setRequests] = useState<JoinRequest[]>([]);
|
const [requests, setRequests] = useState<JoinRequest[]>([]);
|
||||||
|
const [userRequest, setUserRequest] = useState<UserJoinRequest[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [userRequest, setUserRequest] = useState<UserJoinRequest[]>([])
|
|
||||||
|
// --- HOOKS ---
|
||||||
|
|
||||||
|
// Effect to set the initial tab from route params
|
||||||
|
useEffect(() => {
|
||||||
|
const passedTab = route.params?.actionTab;
|
||||||
|
if (passedTab) {
|
||||||
|
const availableTabs = user.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"];
|
||||||
|
if (availableTabs.includes(passedTab)) {
|
||||||
|
setActiveTab(passedTab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [route.params?.actionTab, user.role]);
|
||||||
|
|
||||||
|
// --- FIXED: fetchData dependency array is corrected ---
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
|
// Check for loading state at the start to prevent multiple simultaneous fetches
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (userRole.role === "ORGANIZER") {
|
if (user.role === "ORGANIZER") {
|
||||||
// Fetch all data concurrently for organizers
|
|
||||||
const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([
|
const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([
|
||||||
apiCall(MY_GROUPS_URL, "GET") as Promise<ApiResponse<MyGroup[]>>,
|
apiCall(MY_GROUPS_URL, "GET") as Promise<ApiResponse<MyGroup[]>>,
|
||||||
apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>,
|
apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>,
|
||||||
apiCall(REQUESTS_URL, "GET") as Promise<ApiResponse<JoinRequest[]>>,
|
apiCall(REQUESTS_URL, "GET") as Promise<ApiResponse<JoinRequest[]>>,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log("My Groups:", myGroupsRes.data);
|
|
||||||
console.log("Joined Groups:", joinedGroupsRes.data);
|
|
||||||
console.log("Requests:", requestsRes.data);
|
|
||||||
|
|
||||||
setMyGroups(myGroupsRes.data || []);
|
setMyGroups(myGroupsRes.data || []);
|
||||||
setJoinedGroups(joinedGroupsRes.data || []);
|
setJoinedGroups(joinedGroupsRes.data || []);
|
||||||
setRequests(requestsRes.data || []);
|
setRequests(requestsRes.data || []);
|
||||||
} else {
|
} else {
|
||||||
// Fetch only joined groups and user requests for non-organizers
|
|
||||||
const [joinedGroupsRes, userRequestRes] = await Promise.all([
|
const [joinedGroupsRes, userRequestRes] = await Promise.all([
|
||||||
apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>,
|
apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>,
|
||||||
apiCall(USER_REQUEST, "GET") as Promise<ApiResponse<UserJoinRequest[]>>,
|
apiCall(USER_REQUEST, "GET") as Promise<ApiResponse<UserJoinRequest[]>>,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log("Joined Groups:", joinedGroupsRes.data);
|
|
||||||
console.log("User Requests:", userRequestRes.data);
|
|
||||||
|
|
||||||
setJoinedGroups(joinedGroupsRes.data || []);
|
setJoinedGroups(joinedGroupsRes.data || []);
|
||||||
setUserRequest(userRequestRes.data || []);
|
setUserRequest(userRequestRes.data || []);
|
||||||
}
|
}
|
||||||
@@ -81,109 +87,64 @@ const ManagementScreen: FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [userRole, loading]);
|
// The only external dependency that should trigger a re-fetch is the user's role.
|
||||||
|
}, [user.role]); // **FIXED**: Removed `loading` from the dependency array
|
||||||
|
|
||||||
useFocusEffect(useCallback(() => { fetchData(); }, []));
|
// --- FIXED: useFocusEffect now has a stable dependency ---
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]) // This will now only re-run if fetchData (and thus user.role) changes.
|
||||||
|
);
|
||||||
|
|
||||||
const handleAction = async (url: string, body: object, successMessage: string, failureMessage: string, action: string) => {
|
// --- HANDLERS (No changes needed here) ---
|
||||||
// ... (logic remains the same)
|
const handleViewGroup = (item: JoinedGroup) => {
|
||||||
console.log(url)
|
// Assuming you have an 'Invite' screen in your RootTabParamList or another navigator
|
||||||
|
// navigation.navigate('Invite' as any, { groupId: item.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAction = async (url: string, successMessage: string, failureMessage: string) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true); // Optionally show a loading state during the action
|
// No need to set loading here if the whole screen is already loading
|
||||||
// Make the POST request with an empty body
|
|
||||||
const res = await apiCall(url, "PUT", {}) as ApiResponse<null> | null;
|
const res = await apiCall(url, "PUT", {}) as ApiResponse<null> | null;
|
||||||
|
|
||||||
if (res && res.statusCode === 200) {
|
if (res && res.statusCode === 200) {
|
||||||
// Use the success message from the API response
|
Alert.alert("Success", res.message || successMessage);
|
||||||
Alert.alert("Success", res.message || `Request ${action}d successfully.`);
|
fetchData(); // Refresh data after action
|
||||||
fetchData(); // Refresh the list of requests
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res?.message || failureMessage);
|
throw new Error(res?.message || failureMessage);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Alert.alert("Error", err.message || "An unexpected error occurred.");
|
Alert.alert("Error", err.message || "An unexpected error occurred.");
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApprove = (req: JoinRequest) => handleAction(APPROVE_REQUEST_URL + req.requestId + "/approve", { requestId: req.requestId }, `${req.userName} has been added.`, "Failed to approve request.", "Approve");
|
const handleApprove = (req: JoinRequest) => handleAction(APPROVE_REQUEST_URL + req.requestId + "/approve", `${req.userName} has been added.`, "Failed to approve request.");
|
||||||
const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL + req.requestId + "/reject?reason=/" + " ", { requestId: req.requestId }, `Request from ${req.userName} has been declined.`, "Failed to decline request.", "Decline");
|
const handleDecline = (req: JoinRequest) => handleAction(DECLINE_REQUEST_URL + req.requestId + "/reject?reason=/", `Request from ${req.userName} has been declined.`, "Failed to decline request.");
|
||||||
const handleManageGroup = (group: MyGroup) => navigation.navigate('EditGroup', { groupId: group.id });
|
const handleManageGroup = (group: MyGroup) => navigation.navigate('ManagementTabs', { groupId: group.id });
|
||||||
|
|
||||||
|
// --- RENDER LOGIC ---
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
if (error) {
|
if (error) return <Text style={styles.emptyText}>{error}</Text>;
|
||||||
return <Text style={styles.emptyText}>{error}</Text>;
|
// Improved loading logic to avoid hiding existing data during a refresh
|
||||||
|
if (loading && myGroups.length === 0 && joinedGroups.length === 0 && requests.length === 0) {
|
||||||
|
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case "My Groups":
|
case "My Groups":
|
||||||
if (loading) {
|
return <FlatList data={myGroups} renderItem={({ item }) => <MyGroupCard item={item} onManage={handleManageGroup} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't created any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||||
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={myGroups}
|
|
||||||
renderItem={({ item }: ListRenderItemInfo<MyGroup>) => <MyGroupCard item={item} onManage={handleManageGroup} />}
|
|
||||||
keyExtractor={(item) => item.id}
|
|
||||||
ListEmptyComponent={<Text style={styles.emptyText}>You haven't created any groups yet.</Text>}
|
|
||||||
onRefresh={fetchData}
|
|
||||||
refreshing={loading} // This handles the "continuing loading" on pull-to-refresh
|
|
||||||
contentContainerStyle={{ paddingBottom: 100 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "Joined Groups":
|
case "Joined Groups":
|
||||||
if (loading && joinedGroups.length === 0) {
|
return <FlatList data={joinedGroups} renderItem={({ item }) => <JoinedGroupCard item={item} onViewPress={() => handleViewGroup(item)} />} keyExtractor={(item) => item.id} ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||||
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={joinedGroups}
|
|
||||||
renderItem={({ item }: ListRenderItemInfo<JoinedGroup>) => <JoinedGroupCard item={item} />}
|
|
||||||
keyExtractor={(item) => item.id}
|
|
||||||
ListEmptyComponent={<Text style={styles.emptyText}>You haven't joined any groups yet.</Text>}
|
|
||||||
onRefresh={fetchData}
|
|
||||||
refreshing={loading} // This handles the "continuing loading" on pull-to-refresh
|
|
||||||
contentContainerStyle={{ paddingBottom: 100 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "Requests":
|
case "Requests":
|
||||||
if (loading && requests.length === 0) {
|
return <FlatList data={requests} renderItem={({ item }) => <RequestCard item={item} onApprove={handleApprove} onDecline={handleDecline} />} keyExtractor={(item) => item.requestId} ListEmptyComponent={<Text style={styles.emptyText}>No pending join requests.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||||
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={requests}
|
|
||||||
renderItem={({ item }: ListRenderItemInfo<JoinRequest>) => <RequestCard item={item} onApprove={handleApprove} onDecline={handleDecline} />}
|
|
||||||
keyExtractor={(item) => item.requestId}
|
|
||||||
ListEmptyComponent={<Text style={styles.emptyText}>No pending join requests.</Text>}
|
|
||||||
onRefresh={fetchData}
|
|
||||||
refreshing={loading} // This handles the "continuing loading" on pull-to-refresh
|
|
||||||
contentContainerStyle={{ paddingBottom: 100 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "Your Requests":
|
case "Your Requests":
|
||||||
if (loading && requests.length === 0) {
|
return <FlatList data={userRequest} renderItem={({ item }) => <UserRequested item={item} />} keyExtractor={(item) => item.requestId} ListEmptyComponent={<Text style={styles.emptyText}>You haven't sent any join requests.</Text>} onRefresh={fetchData} refreshing={loading} contentContainerStyle={{ paddingBottom: 100 }} />;
|
||||||
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={userRequest}
|
|
||||||
renderItem={({ item }: ListRenderItemInfo<UserJoinRequest>) => <UserRequested item={item} />}
|
|
||||||
keyExtractor={(item) => item.requestId}
|
|
||||||
ListEmptyComponent={<Text style={styles.emptyText}>No pending join requests.</Text>}
|
|
||||||
onRefresh={fetchData}
|
|
||||||
refreshing={loading} // This handles the "continuing loading" on pull-to-refresh
|
|
||||||
contentContainerStyle={{ paddingBottom: 100 }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabs: ActiveTab[] = userRole.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"];
|
const tabs: ActiveTab[] = user.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"];
|
||||||
|
|
||||||
console.log("user request : ", userRequest)
|
|
||||||
return (
|
return (
|
||||||
<AppView style={styles.container} paddingHorizontal={0}>
|
<AppView style={styles.container} paddingHorizontal={0}>
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
@@ -197,32 +158,6 @@ const ManagementScreen: FC = () => {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.contentArea}>
|
<View style={styles.contentArea}>
|
||||||
{
|
|
||||||
activeTab === "Requests" && userRole === "ORGANIZER" ? (
|
|
||||||
<TouchableOpacity
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
bottom: 85,
|
|
||||||
right: 20,
|
|
||||||
backgroundColor: colors.pastelOrange,
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
borderRadius: 30,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
shadowColor: "#000",
|
|
||||||
shadowOpacity: 0.2,
|
|
||||||
shadowRadius: 5,
|
|
||||||
shadowOffset: { width: 0, height: 3 },
|
|
||||||
elevation: 5, // for Android shadow
|
|
||||||
}}
|
|
||||||
onPress={() => navigation.navigate("YourContacts" as never)}
|
|
||||||
>
|
|
||||||
<Ionicons name="person-add" size={28} color={colors.textDark} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</View>
|
</View>
|
||||||
</AppView>
|
</AppView>
|
||||||
@@ -230,59 +165,17 @@ const ManagementScreen: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: { flex: 1, backgroundColor: '#f5f6fa' },
|
||||||
flex: 1,
|
header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
|
||||||
backgroundColor: '#f5f6fa'
|
title: { fontSize: 22, fontWeight: "600", color: colors.Settings },
|
||||||
},
|
segmentedControl: { flexDirection: 'row', backgroundColor: '#e9ecef', borderRadius: 10, marginHorizontal: 20, marginBottom: 20, padding: 3 },
|
||||||
header: {
|
segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 },
|
||||||
paddingHorizontal: 20,
|
segmentActive: { backgroundColor: colors.pastelOrange, elevation: 3, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
|
||||||
paddingBottom: 15,
|
segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
|
||||||
},
|
segmentTextActive: { color: colors.textDark },
|
||||||
title: {
|
contentArea: { flex: 1, paddingHorizontal: 20 },
|
||||||
fontSize: 22, fontWeight: "600", color: colors.Settings
|
loader: { marginTop: 50 },
|
||||||
},
|
emptyText: { textAlign: 'center', marginTop: 60, fontSize: 16, color: '#6c757d' },
|
||||||
segmentedControl: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
backgroundColor: '#e9ecef',
|
|
||||||
borderRadius: 10,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
marginBottom: 20,
|
|
||||||
padding: 3,
|
|
||||||
},
|
|
||||||
segmentButton: {
|
|
||||||
flex: 1,
|
|
||||||
paddingVertical: 12,
|
|
||||||
alignItems: 'center',
|
|
||||||
borderRadius: 8,
|
|
||||||
},
|
|
||||||
segmentActive: {
|
|
||||||
backgroundColor: colors.pastelOrange, // Example color
|
|
||||||
elevation: 3,
|
|
||||||
shadowColor: '#000',
|
|
||||||
shadowOpacity: 0.1,
|
|
||||||
shadowRadius: 5,
|
|
||||||
},
|
|
||||||
segmentText: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
color: '#6c757d',
|
|
||||||
},
|
|
||||||
segmentTextActive: {
|
|
||||||
color: colors.textDark, // Example color
|
|
||||||
},
|
|
||||||
contentArea: {
|
|
||||||
flex: 1,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
},
|
|
||||||
loader: {
|
|
||||||
marginTop: 50,
|
|
||||||
},
|
|
||||||
emptyText: {
|
|
||||||
textAlign: 'center',
|
|
||||||
marginTop: 60,
|
|
||||||
fontSize: 16,
|
|
||||||
color: '#6c757d',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default ManagementScreen;
|
export default ManagementScreen;
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
|
import { View, Text, StyleSheet, Platform, TouchableOpacity } from 'react-native';
|
||||||
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
import Ionicons from 'react-native-vector-icons/Ionicons'; // Correct import for react-native-vector-icons
|
||||||
|
|
||||||
|
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
|
import { RootStackParamList } from '../../navigation/AppNavigator'; // Adjust path
|
||||||
|
import EditGroupScreen from './EditGroupScreen'; // Your screen component
|
||||||
|
import Payments from './Payments';
|
||||||
|
import Member from './Member';
|
||||||
|
|
||||||
|
type Props = NativeStackScreenProps<RootStackParamList, 'ManagementTabs'>;
|
||||||
|
|
||||||
|
const Tab = createBottomTabNavigator();
|
||||||
|
|
||||||
|
// --- Custom Floating Tab Bar Component ---
|
||||||
|
import type { BottomTabBarProps } from '@react-navigation/bottom-tabs';
|
||||||
|
|
||||||
|
const FloatingTabBar = ({ state, descriptors, navigation }: BottomTabBarProps) => {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.tabBarContainer, { bottom: insets.bottom > 0 ? insets.bottom : 16 }]}>
|
||||||
|
{state.routes.map((route, index) => {
|
||||||
|
const { options } = descriptors[route.key];
|
||||||
|
const label = options.tabBarLabel ?? options.title ?? route.name;
|
||||||
|
// A custom option to pass the icon name
|
||||||
|
const iconName = options.tabBarIconName ?? 'help-circle';
|
||||||
|
|
||||||
|
const isFocused = state.index === index;
|
||||||
|
|
||||||
|
const onPress = () => {
|
||||||
|
const event = navigation.emit({
|
||||||
|
type: 'tabPress',
|
||||||
|
target: route.key,
|
||||||
|
canPreventDefault: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isFocused && !event.defaultPrevented) {
|
||||||
|
navigation.navigate(route.name, route.params);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLongPress = () => {
|
||||||
|
navigation.emit({
|
||||||
|
type: 'tabLongPress',
|
||||||
|
target: route.key,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={route.key}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityState={isFocused ? { selected: true } : {}}
|
||||||
|
accessibilityLabel={options.tabBarAccessibilityLabel}
|
||||||
|
testID={options.tabBarButtonTestID}
|
||||||
|
onPress={onPress}
|
||||||
|
onLongPress={onLongPress}
|
||||||
|
style={styles.tabItem}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={iconName}
|
||||||
|
size={24}
|
||||||
|
color={isFocused ? '#4F46E5' : '#6B7280'}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.tabLabel, { color: isFocused ? '#4F46E5' : '#6B7280' }]}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Main Tab Navigator Component ---
|
||||||
|
const ManagementTabNavigation: React.FC<Props> = ({ route }) => {
|
||||||
|
const { groupId } = route.params
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tab.Navigator
|
||||||
|
// Assign the custom floating tab bar
|
||||||
|
tabBar={props => <FloatingTabBar {...props} />}
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false, // Hides the default header for all screens in the tab
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab.Screen
|
||||||
|
name="EditGroup"
|
||||||
|
component={EditGroupScreen}
|
||||||
|
// Pass the groupId to the EditGroupScreen as an initial parameter
|
||||||
|
initialParams={{ groupId }}
|
||||||
|
options={{
|
||||||
|
tabBarLabel: 'Settings',
|
||||||
|
// Custom prop to define the icon for our FloatingTabBar
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Payments"
|
||||||
|
component={Payments}
|
||||||
|
// Pass the groupId to the EditGroupScreen as an initial parameter
|
||||||
|
initialParams={{ groupId }}
|
||||||
|
options={{
|
||||||
|
tabBarLabel: 'Payments',
|
||||||
|
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tab.Screen
|
||||||
|
name="Member"
|
||||||
|
component={Member}
|
||||||
|
// Pass the groupId to the EditGroupScreen as an initial parameter
|
||||||
|
initialParams={{ groupId }}
|
||||||
|
options={{
|
||||||
|
tabBarLabel: 'Members',
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* You can easily add more tabs here in the future */}
|
||||||
|
</Tab.Navigator>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Styles ---
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
tabBarContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
position: 'absolute',
|
||||||
|
left: 30,
|
||||||
|
right: 30,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderRadius: 60,
|
||||||
|
paddingVertical: 4,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 4,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 10,
|
||||||
|
elevation: 8,
|
||||||
|
},
|
||||||
|
tabItem: {
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 8,
|
||||||
|
},
|
||||||
|
tabLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
marginTop: 4,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ManagementTabNavigation;
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
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';
|
||||||
|
import { useRoute } from '@react-navigation/native';
|
||||||
|
|
||||||
|
// --- 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 = ({ route }) => {
|
||||||
|
|
||||||
|
const { groupId } = route.params;
|
||||||
|
const [members, setMembers] = useState<Member[]>([]);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const fetchMembers = useCallback(async () => {
|
||||||
|
if (!groupId) {
|
||||||
|
setError('Group ID is missing.');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiCall<ApiResponse>(
|
||||||
|
`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/all/member?groupId=${groupId}`,
|
||||||
|
'GET'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response && response.status === 'success') {
|
||||||
|
setMembers(response.data);
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
throw new Error(response?.message || 'Failed to fetch members.');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
setError(e.message || 'An unexpected network error occurred.');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [groupId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMembers();
|
||||||
|
}, [fetchMembers]);
|
||||||
|
|
||||||
|
// --- Render Functions ---
|
||||||
|
const renderItem = ({ item }: { item: Member }) => {
|
||||||
|
const fullName = `${item.firstName} ${item.lastName || ''}`.trim();
|
||||||
|
const initial = item.firstName ? item.firstName.charAt(0).toUpperCase() : '?';
|
||||||
|
const isOrganizer = item.role === 'ORGANIZER';
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.card}>
|
||||||
|
<View style={styles.avatarPlaceholder}>
|
||||||
|
<Text style={styles.avatarText}>{initial}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.infoContainer}>
|
||||||
|
<View style={{flexDirection:"row",justifyContent:"space-between"}}>
|
||||||
|
<Text style={styles.name}>{fullName}</Text>
|
||||||
|
{isOrganizer && (
|
||||||
|
<View style={styles.organizerIcon}>
|
||||||
|
<Text>Orginzer</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={styles.username}>@{item.username}</Text>
|
||||||
|
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<ActivityIndicator size="large" color="#4F46E5" />
|
||||||
|
<Text style={styles.loadingText}>Loading Members...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<Ionicons name="cloud-offline-outline" size={60} color="#EF4444" />
|
||||||
|
<Text style={styles.errorTitle}>Oops!</Text>
|
||||||
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
|
<TouchableOpacity style={styles.retryButton} onPress={fetchMembers}>
|
||||||
|
<Text style={styles.retryButtonText}>Try Again</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (members.length === 0) {
|
||||||
|
return (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<Ionicons name="people-outline" size={60} color="#9CA3AF" />
|
||||||
|
<Text style={styles.emptyTitle}>No Members Yet</Text>
|
||||||
|
<Text style={styles.emptyText}>Invite people to join your group.</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={members}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
contentContainerStyle={styles.listContainer}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppView>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.headerTitle}>Group Members</Text>
|
||||||
|
<Text style={styles.headerSubtitle}>{members.length} member(s)</Text>
|
||||||
|
</View>
|
||||||
|
{renderContent()}
|
||||||
|
</AppView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Styles ---
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#F3F4F6',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingTop: 20,
|
||||||
|
paddingBottom: 12,
|
||||||
|
backgroundColor: 'white',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#E5E7EB',
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
fontSize: 28,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#111827',
|
||||||
|
},
|
||||||
|
headerSubtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#6B7280',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
listContainer: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
padding: 16,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
shadowColor: '#374151',
|
||||||
|
shadowOpacity: 0.08,
|
||||||
|
shadowRadius: 10,
|
||||||
|
shadowOffset: { width: 0, height: 4 },
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
avatarPlaceholder: {
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
borderRadius: 26,
|
||||||
|
backgroundColor: '#D1D5DB',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 16,
|
||||||
|
},
|
||||||
|
avatarText: {
|
||||||
|
color: '#4B5563',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
organizerIcon: {
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 2,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: colors.pastelOrange
|
||||||
|
},
|
||||||
|
infoContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#111827',
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#6B7280',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
contactRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
contactText: {
|
||||||
|
marginLeft: 8,
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#374151',
|
||||||
|
},
|
||||||
|
menuButton: {
|
||||||
|
padding: 8,
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
centered: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: 20,
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 10,
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#6B7280',
|
||||||
|
},
|
||||||
|
errorTitle: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#EF4444',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#374151',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
retryButton: {
|
||||||
|
backgroundColor: '#4F46E5',
|
||||||
|
paddingVertical: 12,
|
||||||
|
paddingHorizontal: 30,
|
||||||
|
borderRadius: 8,
|
||||||
|
},
|
||||||
|
retryButtonText: {
|
||||||
|
color: 'white',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
emptyTitle: {
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#374151',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#6B7280',
|
||||||
|
textAlign: 'center',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MemberScreen;
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
|
import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity } from 'react-native';
|
||||||
|
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
||||||
|
import colors from '../../components/colors';
|
||||||
|
import AppView from '../../components/AppView';
|
||||||
|
import { apiCall } from '../../utils/ApiHelper';
|
||||||
|
|
||||||
|
// --- TYPE DEFINITIONS ---
|
||||||
|
|
||||||
|
// Generic API Response Wrapper
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
status: 'success' | 'error';
|
||||||
|
data: T;
|
||||||
|
message?: string;
|
||||||
|
statusCode: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type for an individual participant in the payment details
|
||||||
|
interface Participant {
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
mobile: string;
|
||||||
|
contributionAmount: number;
|
||||||
|
paidAmount: number;
|
||||||
|
status: 'SUCCESS' | 'PENDING';
|
||||||
|
lateFee: number;
|
||||||
|
daysLate: number;
|
||||||
|
lateFeeWaived: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fully typed payment details based on your JSON response
|
||||||
|
interface PaymentDetails {
|
||||||
|
groupId: string;
|
||||||
|
groupName: string;
|
||||||
|
currentCycle: number;
|
||||||
|
cycleType: string;
|
||||||
|
contributionAmount: number;
|
||||||
|
totalContributedAmount: number;
|
||||||
|
lateFeeMode: string;
|
||||||
|
lateFeeValue: number;
|
||||||
|
notReceivedAmount: number;
|
||||||
|
totalLateFee: number;
|
||||||
|
totalDueWithLateFee: number;
|
||||||
|
participants: Participant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assuming your navigation stack params look something like this
|
||||||
|
type PaymentStackParamList = {
|
||||||
|
Payments: { groupId: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
type PaymentsRouteProp = RouteProp<PaymentStackParamList, 'Payments'>;
|
||||||
|
type PaymentsNavigationProp = StackNavigationProp<PaymentStackParamList>;
|
||||||
|
|
||||||
|
const Payments: React.FC = () => {
|
||||||
|
const route = useRoute<PaymentsRouteProp>();
|
||||||
|
const navigation = useNavigation<PaymentsNavigationProp>();
|
||||||
|
const { groupId } = route.params;
|
||||||
|
|
||||||
|
const [paymentDetails, setPaymentDetails] = useState<PaymentDetails | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
|
|
||||||
|
const fetchPaymentDetails = useCallback(async () => {
|
||||||
|
if (!refreshing) setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiCall(
|
||||||
|
`https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/current/payment/details/${groupId}`,
|
||||||
|
'GET'
|
||||||
|
) as ApiResponse<PaymentDetails>;
|
||||||
|
|
||||||
|
if (response && response.statusCode === 200) {
|
||||||
|
setPaymentDetails(response.data);
|
||||||
|
} else {
|
||||||
|
Alert.alert("Error", response?.message || "Failed to fetch payment details.");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Fetch payment details error:", error);
|
||||||
|
Alert.alert("Error", error.message || "An unexpected error occurred.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
|
}, [groupId, refreshing]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPaymentDetails();
|
||||||
|
}, [fetchPaymentDetails]);
|
||||||
|
|
||||||
|
const onRefresh = () => {
|
||||||
|
setRefreshing(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- ENHANCED: renderParticipant now shows late fee details ---
|
||||||
|
const renderParticipant = (participant: Participant) => {
|
||||||
|
const isPending = participant.status === 'PENDING';
|
||||||
|
return (
|
||||||
|
<View key={participant.userId} style={styles.participantCard}>
|
||||||
|
<View style={styles.participantInfo}>
|
||||||
|
<Text style={styles.participantName}>{participant.firstName} {participant.lastName}</Text>
|
||||||
|
<Text style={styles.participantMobile}>{participant.mobile}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.paymentStatus}>
|
||||||
|
<Text style={[
|
||||||
|
styles.statusText,
|
||||||
|
{ color: isPending ? '#e0d626ff' : '#18b107ff' }
|
||||||
|
]}>
|
||||||
|
{participant.status}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.amountText}>
|
||||||
|
{isPending ?`₹${participant.payAmount}`:`₹${participant.paidAmount}`}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* --- NEW: Conditionally render late payment stats --- */}
|
||||||
|
{isPending && participant.daysLate > 0 && (
|
||||||
|
<View style={styles.lateFeeContainer}>
|
||||||
|
<Text style={styles.lateFeeText}>Late Fee: ₹{participant.lateFee}</Text>
|
||||||
|
<Text style={styles.lateFeeText}>Days Late: {participant.daysLate}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading && !refreshing) {
|
||||||
|
return <ActivityIndicator style={styles.centered} size="large" color={colors.primary} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!paymentDetails) {
|
||||||
|
return (
|
||||||
|
<View style={styles.centered}>
|
||||||
|
<Text>No payment details found.</Text>
|
||||||
|
<TouchableOpacity onPress={fetchPaymentDetails} style={styles.retryButton}>
|
||||||
|
<Text style={styles.retryButtonText}>Tap to retry</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppView paddingHorizontal={0}>
|
||||||
|
<ScrollView
|
||||||
|
style={styles.container}
|
||||||
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[colors.primary]} tintColor={colors.primary} />}
|
||||||
|
>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.headerTitle}>{paymentDetails.groupName}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.summaryContainer}>
|
||||||
|
<View style={styles.summaryBox}>
|
||||||
|
<Text style={styles.summaryLabel}>Current Cycle</Text>
|
||||||
|
<Text style={styles.summaryValue}>{paymentDetails.currentCycle}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryBox}>
|
||||||
|
<Text style={styles.summaryLabel}>Contribution</Text>
|
||||||
|
<Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.summaryBox}>
|
||||||
|
<Text style={styles.summaryLabel}>Total Due</Text>
|
||||||
|
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails.totalDueWithLateFee}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.sectionTitle}>Participants</Text>
|
||||||
|
{paymentDetails.participants.map(renderParticipant)}
|
||||||
|
</ScrollView>
|
||||||
|
</AppView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
centered: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#f4f6f8',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: '#f4f6f8',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
padding: 20,
|
||||||
|
paddingBottom: 15,
|
||||||
|
},
|
||||||
|
headerTitle: {
|
||||||
|
color: '#000000',
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
summaryContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
padding: 20,
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
marginHorizontal: 15,
|
||||||
|
borderRadius: 12,
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOpacity: 0.05,
|
||||||
|
shadowRadius: 10,
|
||||||
|
},
|
||||||
|
summaryBox: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
summaryLabel: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#6c757d',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
summaryValue: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: colors.primary,
|
||||||
|
},
|
||||||
|
sectionTitle: {
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
marginLeft: 20,
|
||||||
|
marginTop: 20,
|
||||||
|
marginBottom: 10,
|
||||||
|
color: '#343a40',
|
||||||
|
},
|
||||||
|
participantCard: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
marginHorizontal: 15,
|
||||||
|
marginBottom: 10,
|
||||||
|
borderRadius: 10,
|
||||||
|
padding: 15,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
elevation: 1,
|
||||||
|
},
|
||||||
|
participantInfo: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
participantName: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#212529',
|
||||||
|
},
|
||||||
|
participantMobile: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#6c757d',
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
paymentStatus: {
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
statusText: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
},
|
||||||
|
amountText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#495057',
|
||||||
|
marginTop: 2,
|
||||||
|
},
|
||||||
|
// --- NEW STYLES ---
|
||||||
|
lateFeeContainer: {
|
||||||
|
marginTop: 8,
|
||||||
|
paddingTop: 5,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: '#e9ecef',
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
},
|
||||||
|
lateFeeText: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: colors.danger,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
retryButton: {
|
||||||
|
marginTop: 20,
|
||||||
|
paddingVertical: 10,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: colors.primary,
|
||||||
|
},
|
||||||
|
retryButtonText: {
|
||||||
|
color: '#fff',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Payments;
|
||||||
|
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
Platform,
|
Platform,
|
||||||
SafeAreaView,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import * as Keychain from "react-native-keychain";
|
import * as Keychain from "react-native-keychain";
|
||||||
@@ -16,17 +15,17 @@ import { CustomInput, CustomButton } from "../../components/common/Custom";
|
|||||||
import colors from "../../components/colors";
|
import colors from "../../components/colors";
|
||||||
import { useNavigation } from "@react-navigation/native";
|
import { useNavigation } from "@react-navigation/native";
|
||||||
import AppView from "../../components/AppView";
|
import AppView from "../../components/AppView";
|
||||||
import Ionicons from "@react-native-vector-icons/ionicons";
|
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
||||||
|
|
||||||
const BecomeOrg = () => {
|
const BecomeOrg = () => {
|
||||||
const navigation = useNavigation<any>();
|
const navigation = useNavigation<any>();
|
||||||
const [bankAccountNumber, setBankAccountNumber] = useState("");
|
const [idName, setIdName] = useState("");
|
||||||
const [bankName, setBankName] = useState("");
|
const [idNumber, setIdNumber] = useState("");
|
||||||
const [branchCode, setBranchCode] = useState("");
|
const [issueDate, setIssueDate] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!bankAccountNumber || !bankName || !branchCode) {
|
if (!idName || !idNumber || !issueDate) {
|
||||||
Alert.alert("Error", "Please fill all fields");
|
Alert.alert("Error", "Please fill all fields");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -35,7 +34,10 @@ const BecomeOrg = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const credentials = await Keychain.getGenericPassword();
|
const credentials = await Keychain.getGenericPassword();
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
Alert.alert("Error", "You are not logged in");
|
Alert.alert("Error", "You are not logged in. Please log in to continue.");
|
||||||
|
setLoading(false);
|
||||||
|
// Optionally navigate to login screen
|
||||||
|
// navigation.navigate("Login");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const token = credentials.password;
|
const token = credentials.password;
|
||||||
@@ -45,31 +47,31 @@ const BecomeOrg = () => {
|
|||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Accept: "*/*",
|
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
bankAccountNumber,
|
idName,
|
||||||
bankName,
|
idNumber,
|
||||||
branchCode,
|
issueDate,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.status === "success") {
|
|
||||||
Alert.alert("Success", "Organizer registration applied successfully");
|
if (response.ok && result.status === "success") {
|
||||||
setBankAccountNumber("");
|
Alert.alert("Success", "Organizer registration application submitted successfully!");
|
||||||
setBankName("");
|
setIdName("");
|
||||||
setBranchCode("");
|
setIdNumber("");
|
||||||
|
setIssueDate("");
|
||||||
navigation.navigate("Home" as never);
|
navigation.navigate("Home" as never);
|
||||||
} else {
|
} else {
|
||||||
Alert.alert("Error", result.message || "Failed to apply");
|
Alert.alert("Error", result.message || "Failed to submit application");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Registration error:", error);
|
console.error("❌ Registration error:", error);
|
||||||
Alert.alert("Error", "Something went wrong");
|
Alert.alert("Error", "An unexpected error occurred. Please try again.");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -78,11 +80,11 @@ const BecomeOrg = () => {
|
|||||||
return (
|
return (
|
||||||
<AppView barStyle="dark-content">
|
<AppView barStyle="dark-content">
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<TouchableOpacity onPress={() => navigation.replace("Home" as never)} >
|
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
|
||||||
<Ionicons name="arrow-back" size={24} color={'#000'} />
|
<Ionicons name="arrow-back" size={24} color={'#000'} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<Text style={styles.headerTitle}>Become Organizer</Text>
|
<Text style={styles.headerTitle}>Become an Organizer</Text>
|
||||||
|
<View style={styles.placeholder} />
|
||||||
</View>
|
</View>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
@@ -93,37 +95,42 @@ const BecomeOrg = () => {
|
|||||||
contentContainerStyle={styles.container}
|
contentContainerStyle={styles.container}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<Text style={styles.title}>Organizer Registration</Text>
|
<Text style={styles.title}>Submit Your ID for Verification</Text>
|
||||||
|
<Text style={styles.subtitle}>
|
||||||
|
Please provide your official identification details to become a verified organizer.
|
||||||
|
</Text>
|
||||||
|
|
||||||
<CustomInput
|
<CustomInput
|
||||||
placeholder="Bank Account Number"
|
placeholder="ID Name (e.g., Aadhaar, PAN Card)"
|
||||||
keyboardType="numeric"
|
value={idName}
|
||||||
value={bankAccountNumber}
|
onChangeText={setIdName}
|
||||||
onChangeText={setBankAccountNumber}
|
icon="person-outline"
|
||||||
icon="card-outline" error={""} />
|
error={""}
|
||||||
|
/>
|
||||||
|
|
||||||
<CustomInput
|
<CustomInput
|
||||||
placeholder="Bank Name"
|
placeholder="ID Number"
|
||||||
value={bankName}
|
value={idNumber}
|
||||||
onChangeText={setBankName}
|
onChangeText={setIdNumber}
|
||||||
icon="business-outline"
|
icon="card-outline"
|
||||||
error=""
|
error=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CustomInput
|
<CustomInput
|
||||||
placeholder="Branch Code"
|
placeholder="Issue Date (YYYY-MM-DD)"
|
||||||
value={branchCode}
|
value={issueDate}
|
||||||
onChangeText={setBranchCode}
|
onChangeText={setIssueDate}
|
||||||
icon="key-outline"
|
icon="calendar-outline"
|
||||||
error=""
|
error=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CustomButton
|
<CustomButton
|
||||||
title={loading ? "Submitting..." : "Request Organizer Role"}
|
title={loading ? "Submitting..." : "Request Organizer Role"}
|
||||||
onPress={handleSubmit}
|
onPress={handleSubmit}
|
||||||
|
|
||||||
icon="send-outline"
|
icon="send-outline"
|
||||||
buttonBackground={colors.pastelBlue}
|
buttonBackground={colors.pastelBlue}
|
||||||
style={{ opacity: loading ? 0.6 : 1 }}
|
style={{ opacity: loading ? 0.6 : 1, marginTop: 20 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -148,23 +155,38 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
fontSize: 22,
|
fontSize: 24,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
marginBottom: 20,
|
marginBottom: 10,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
color: colors.textDark,
|
color: colors.textDark,
|
||||||
},
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
textAlign: "center",
|
||||||
|
color: colors.textDark,
|
||||||
|
marginBottom: 30,
|
||||||
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: 16,
|
paddingHorizontal: 16,
|
||||||
borderBottomLeftRadius: 20,
|
paddingVertical: 12,
|
||||||
borderBottomRightRadius: 20,
|
backgroundColor: '#FFF',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: '#E0E0E0',
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
padding: 5,
|
||||||
},
|
},
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
fontSize: 22,
|
flex: 1,
|
||||||
fontWeight: "700",
|
fontSize: 20,
|
||||||
color: "#000"
|
fontWeight: "600",
|
||||||
|
color: "#000",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
width: 24, // to balance the back button
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+12
-2
@@ -19,7 +19,8 @@ export async function apiCall<T>(
|
|||||||
try {
|
try {
|
||||||
// Get saved credentials from Keychain
|
// Get saved credentials from Keychain
|
||||||
const creds = await Keychain.getGenericPassword();
|
const creds = await Keychain.getGenericPassword();
|
||||||
// console.log("hihihi", creds);
|
console.log("Keychain Credentials Found:", creds); // Log if creds were found
|
||||||
|
|
||||||
const headers = {
|
const headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(creds ? { Authorization: `Bearer ${creds.password}` } : {}),
|
...(creds ? { Authorization: `Bearer ${creds.password}` } : {}),
|
||||||
@@ -34,6 +35,12 @@ export async function apiCall<T>(
|
|||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Response Logging ---
|
||||||
|
console.log(`--- API Response: [${response.status}] ${url} ---`);
|
||||||
|
// Uncomment the line below to see the full response data in logs
|
||||||
|
// console.log("Response Data:", JSON.stringify(response.data, null, 2));
|
||||||
|
console.log("---------------------------------------");
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
@@ -53,6 +60,9 @@ export async function apiCall<T>(
|
|||||||
// Something else went wrong while setting up the request
|
// Something else went wrong while setting up the request
|
||||||
console.error("API Error:", error);
|
console.error("API Error:", error);
|
||||||
}
|
}
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+105735
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user