181 lines
9.1 KiB
TypeScript
181 lines
9.1 KiB
TypeScript
// src/screens/ManagementScreen.tsx
|
|
|
|
import React, { useState, useCallback, FC, useEffect } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
FlatList,
|
|
TouchableOpacity,
|
|
ActivityIndicator,
|
|
Alert,
|
|
} from "react-native";
|
|
import { useNavigation, useFocusEffect, useRoute, RouteProp } from "@react-navigation/native";
|
|
import { StackNavigationProp } from "@react-navigation/stack";
|
|
import { APPROVE_REQUEST_URL, DECLINE_REQUEST_URL, GET_JOINED_GROUP_URL, MY_GROUPS_URL, REQUESTS_URL, USER_REQUEST } from "../../utils/ApiUrls";
|
|
import colors from "../../components/colors";
|
|
import AppView from "../../components/AppView";
|
|
import { apiCall } from "../../utils/ApiHelper";
|
|
import { MyGroup, JoinedGroup, JoinRequest, ApiResponse, UserJoinRequest } from '../../types'; // Adjust import path
|
|
import { MyGroupCard } from "../../components/MyGroupCard";
|
|
import { JoinedGroupCard } from "../../components/JoinedGroupCard";
|
|
import { RequestCard } from "../../components/RequestCard";
|
|
import { useSelector } from "react-redux";
|
|
import { UserRequested } from "../../components/UserRequested";
|
|
import { RootTabParamList } from "../../navigation/BottomTabs"; // Adjust import path
|
|
|
|
// --- TYPE DEFINITIONS ---
|
|
type GroupManagementNavProp = StackNavigationProp<RootTabParamList, 'Management'>;
|
|
type ManagementRouteProp = RouteProp<RootTabParamList, 'Management'>;
|
|
type ActiveTab = "My Groups" | "Joined Groups" | "Requests" | "Your Requests";
|
|
|
|
const ManagementScreen: FC = () => {
|
|
const user = useSelector((state: any) => state.user);
|
|
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 [joinedGroups, setJoinedGroups] = useState<JoinedGroup[]>([]);
|
|
const [requests, setRequests] = useState<JoinRequest[]>([]);
|
|
const [userRequest, setUserRequest] = useState<UserJoinRequest[]>([]);
|
|
const [loading, setLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// --- 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 () => {
|
|
// Check for loading state at the start to prevent multiple simultaneous fetches
|
|
if (loading) return;
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
if (user.role === "ORGANIZER") {
|
|
const [myGroupsRes, joinedGroupsRes, requestsRes] = await Promise.all([
|
|
apiCall(MY_GROUPS_URL, "GET") as Promise<ApiResponse<MyGroup[]>>,
|
|
apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>,
|
|
apiCall(REQUESTS_URL, "GET") as Promise<ApiResponse<JoinRequest[]>>,
|
|
]);
|
|
setMyGroups(myGroupsRes.data || []);
|
|
setJoinedGroups(joinedGroupsRes.data || []);
|
|
setRequests(requestsRes.data || []);
|
|
} else {
|
|
const [joinedGroupsRes, userRequestRes] = await Promise.all([
|
|
apiCall(GET_JOINED_GROUP_URL, "GET") as Promise<ApiResponse<JoinedGroup[]>>,
|
|
apiCall(USER_REQUEST, "GET") as Promise<ApiResponse<UserJoinRequest[]>>,
|
|
]);
|
|
setJoinedGroups(joinedGroupsRes.data || []);
|
|
setUserRequest(userRequestRes.data || []);
|
|
}
|
|
} catch (e) {
|
|
console.error("Fetch Data Error:", e);
|
|
setError("Failed to fetch data. Please pull to refresh.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
// The only external dependency that should trigger a re-fetch is the user's role.
|
|
}, [user.role]); // **FIXED**: Removed `loading` from the dependency array
|
|
|
|
// --- FIXED: useFocusEffect now has a stable dependency ---
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
fetchData();
|
|
}, [fetchData]) // This will now only re-run if fetchData (and thus user.role) changes.
|
|
);
|
|
|
|
// --- HANDLERS (No changes needed here) ---
|
|
const handleViewGroup = (item: JoinedGroup) => {
|
|
// 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 {
|
|
// No need to set loading here if the whole screen is already loading
|
|
const res = await apiCall(url, "PUT", {}) as ApiResponse<null> | null;
|
|
if (res && res.statusCode === 200) {
|
|
Alert.alert("Success", res.message || successMessage);
|
|
fetchData(); // Refresh data after action
|
|
} else {
|
|
throw new Error(res?.message || failureMessage);
|
|
}
|
|
} catch (err: any) {
|
|
Alert.alert("Error", err.message || "An unexpected error occurred.");
|
|
}
|
|
};
|
|
|
|
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=/", `Request from ${req.userName} has been declined.`, "Failed to decline request.");
|
|
const handleManageGroup = (group: MyGroup) => navigation.navigate('ManagementTabs', { groupId: group.id });
|
|
|
|
// --- RENDER LOGIC ---
|
|
const renderContent = () => {
|
|
if (error) 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) {
|
|
case "My Groups":
|
|
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 }} />;
|
|
case "Joined Groups":
|
|
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 }} />;
|
|
case "Requests":
|
|
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 }} />;
|
|
case "Your Requests":
|
|
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 }} />;
|
|
default: return null;
|
|
}
|
|
};
|
|
|
|
const tabs: ActiveTab[] = user.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"];
|
|
|
|
return (
|
|
<AppView style={styles.container} paddingHorizontal={0}>
|
|
<View style={styles.header}>
|
|
<Text style={styles.title}>Group Management</Text>
|
|
</View>
|
|
<View style={styles.segmentedControl}>
|
|
{tabs.map((tab) => (
|
|
<TouchableOpacity key={tab} style={[styles.segmentButton, activeTab === tab && styles.segmentActive]} onPress={() => setActiveTab(tab)}>
|
|
<Text style={[styles.segmentText, activeTab === tab && styles.segmentTextActive]}>{tab}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
<View style={styles.contentArea}>
|
|
{renderContent()}
|
|
</View>
|
|
</AppView>
|
|
);
|
|
};
|
|
|
|
const styles = StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: '#f5f6fa' },
|
|
header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
|
|
title: { fontSize: 22, fontWeight: "600", color: colors.Settings },
|
|
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, elevation: 3, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
|
|
segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
|
|
segmentTextActive: { color: colors.textDark },
|
|
contentArea: { flex: 1, paddingHorizontal: 20 },
|
|
loader: { marginTop: 50 },
|
|
emptyText: { textAlign: 'center', marginTop: 60, fontSize: 16, color: '#6c757d' },
|
|
});
|
|
|
|
export default ManagementScreen;
|