Files
circle-collect/src/screens/management/ManagementScreen.tsx
T
2026-03-31 13:02:17 +05:30

568 lines
18 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
ScrollView,
ToastAndroid,
RefreshControl,
} from 'react-native';
import { useNavigation, useRoute, RouteProp, useFocusEffect } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { responsiveFontSize } from 'react-native-responsive-dimensions';
import LucideIcon from '../../components/common/LucideIcon';
import {
APPROVE_REQUEST_URL,
DECLINE_REQUEST_URL,
GET_JOINED_GROUP_URL,
MY_GROUPS_URL,
REQUESTS_URL,
USER_REQUEST
} from '../../utils/ApiUrls';
import AppView from '../../components/AppView';
import { useTheme } from '../../hooks/ui/useTheme';
import { useFetch } from '../../hooks/core/useFetch';
import { useUpdate } from '../../hooks/core/useUpdate';
import { MyGroup, JoinRequest } from '../../types';
import MyGroupCard from '../../components/MyGroupCard';
import { JoinedGroupCard } from '../../components/JoinedGroupCard';
import { RequestCard } from '../../components/RequestCard';
import { UserRequested } from '../../components/UserRequested';
import SkeletonLoader from '../../components/common/SkeletonLoader';
import FontsFamily from '../../constents/FontsFamily';
import { RootTabParamList } from '../../navigation/BottomTabs';
type ManagementRouteProp = RouteProp<RootTabParamList, 'Management'>;
type ActiveTab = 'My Groups' | 'Joined Groups' | 'Requests' | 'My Requests';
type RequestSubTab = 'Join Requests' | 'My Requests';
type GroupStatusFilter = 'ALL' | 'PENDING' | 'APPROVED' | 'ACTIVE' | 'DELAY' | 'COMPLETED' | 'REJECTED' | 'CANCELLED';
const GROUP_STATUS_FILTERS: GroupStatusFilter[] = [
'ALL',
'PENDING',
'APPROVED',
'ACTIVE',
'DELAY',
'COMPLETED',
'REJECTED',
'CANCELLED',
];
const JOINED_GROUP_STATUS_FILTERS: GroupStatusFilter[] = [
'ALL',
'ACTIVE',
'DELAY',
'COMPLETED',
'APPROVED'
];
const getListFromResponse = <T,>(response: any): T[] => {
if (!response) return [];
if (Array.isArray(response?.data?.content)) return response.data.content;
if (Array.isArray(response?.data)) return response.data;
return [];
};
const ManagementScreen: React.FC = () => {
const theme = useTheme();
const user = useSelector((state: any) => state.user);
const navigation = useNavigation<any>();
const route = useRoute<ManagementRouteProp>();
const [activeTab, setActiveTab] = useState<ActiveTab>(
user.role === 'ORGANIZER' ? 'My Groups' : 'Joined Groups'
);
const [myGroupStatusFilter, setMyGroupStatusFilter] = useState<GroupStatusFilter>('APPROVED');
const [joinedGroupStatusFilter, setJoinedGroupStatusFilter] = useState<GroupStatusFilter>('ALL');
const [requestSubTab, setRequestSubTab] = useState<RequestSubTab>('Join Requests');
useEffect(() => {
if (!route.params?.actionTab) return;
const routeTab = route.params.actionTab as ActiveTab;
setActiveTab(routeTab);
}, [route.params?.actionTab]);
const myGroupsParams = {
...(myGroupStatusFilter && myGroupStatusFilter !== 'ALL'
? { status: myGroupStatusFilter }
: {}),
page: 0,
size: 10,
};
const joinedGroupsParams = {
...(joinedGroupStatusFilter && joinedGroupStatusFilter !== 'ALL'
? { status: joinedGroupStatusFilter }
: {}),
page: 0,
size: 10,
};
const requestsParams = { page: 0, size: 10 };
const myRequestsParams = { page: 0, size: 10 };
const alwaysFreshFetchOptions = {
staleTime: 0,
gcTime: 0,
refetchOnMount: 'always' as const,
refetchOnReconnect: true,
};
// Data Fetching
const {
data: myGroupsRes,
isLoading: myLoading,
isFetching: myFetching,
refetch: refetchMy,
} = useFetch<any>(
MY_GROUPS_URL,
myGroupsParams,
{ enabled: user.role === 'ORGANIZER' && activeTab === 'My Groups', ...alwaysFreshFetchOptions }
);
const {
data: joinedRes,
isLoading: joinedLoading,
isFetching: joinedFetching,
refetch: refetchJoined,
} = useFetch<any>(
GET_JOINED_GROUP_URL,
joinedGroupsParams,
{ enabled: activeTab === 'Joined Groups', ...alwaysFreshFetchOptions }
);
const {
data: requestsRes,
isLoading: requestsLoading,
isFetching: requestsFetching,
refetch: refetchRequests,
} = useFetch<any>(
REQUESTS_URL,
requestsParams,
{
enabled: user.role === 'ORGANIZER' && activeTab === 'Requests' && requestSubTab === 'Join Requests',
...alwaysFreshFetchOptions,
}
);
const {
data: userReqRes,
isLoading: userReqLoading,
isFetching: userReqFetching,
refetch: refetchUserReq,
} = useFetch<any>(
USER_REQUEST,
myRequestsParams,
{
enabled:
activeTab === 'My Requests' ||
(user.role === 'ORGANIZER' && activeTab === 'Requests' && requestSubTab === 'My Requests'),
...alwaysFreshFetchOptions,
}
);
const { mutate: updateRequest } = useUpdate<any, any>('');
const myGroups = getListFromResponse<MyGroup>(myGroupsRes);
const joinedGroups = getListFromResponse<any>(joinedRes);
const incomingRequests = getListFromResponse<JoinRequest>(requestsRes);
const myRequests = getListFromResponse<any>(userReqRes);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true);
const refreshTasks = [refetchJoined(), refetchUserReq()];
if (user.role === 'ORGANIZER') {
refreshTasks.push(refetchMy(), refetchRequests());
}
await Promise.all(refreshTasks).finally(() => {
setRefreshing(false);
});
}, [refetchMy, refetchJoined, refetchRequests, refetchUserReq, user.role]);
const refetchActiveTab = useCallback(async () => {
if (activeTab === 'My Groups' && user.role === 'ORGANIZER') {
await refetchMy();
return;
}
if (activeTab === 'Joined Groups') {
await refetchJoined();
return;
}
if (activeTab === 'Requests' && user.role === 'ORGANIZER') {
if (requestSubTab === 'Join Requests') {
await refetchRequests();
} else {
await refetchUserReq();
}
return;
}
if (activeTab === 'My Requests') {
await refetchUserReq();
}
}, [activeTab, requestSubTab, refetchMy, refetchJoined, refetchRequests, refetchUserReq, user.role]);
useFocusEffect(
useCallback(() => {
refetchActiveTab();
}, [refetchActiveTab])
);
const tabs: ActiveTab[] =
user.role === 'ORGANIZER'
? ['My Groups', 'Joined Groups', 'Requests']
: ['Joined Groups', 'My Requests'];
const handleApprove = (req: JoinRequest) => {
updateRequest({ resource: `${APPROVE_REQUEST_URL}${req.requestId}/approve`, payload: {} } as any, {
onSuccess: () => {
ToastAndroid.show('Request approved successfully', ToastAndroid.SHORT);
refetchRequests();
},
onError: (err) => ToastAndroid.show(err.message || 'Failed to approve', ToastAndroid.SHORT),
});
};
const handleDecline = (req: JoinRequest) => {
updateRequest({ resource: `${DECLINE_REQUEST_URL}${req.requestId}/reject?reason=no Reason`, payload: {} } as any, {
onSuccess: () => {
ToastAndroid.show('Request declined', ToastAndroid.SHORT);
refetchRequests();
},
onError: (err) => ToastAndroid.show(err.message || 'Failed to decline', ToastAndroid.SHORT),
});
};
const handleManageGroup = (group: MyGroup) => navigation.navigate('ManagementTabs', { groupId: group.id });
const renderSkeletons = () => (
<View style={styles.skeletonContainer}>
{[1, 2, 3].map(i => (
<SkeletonLoader key={i} width="100%" height={120} style={{ marginBottom: 16, borderRadius: 16 }} />
))}
</View>
);
const getFilteredData = () => {
switch (activeTab) {
case 'My Groups': return myGroups;
case 'Joined Groups': return joinedGroups;
case 'Requests':
return requestSubTab === 'Join Requests' ? incomingRequests : myRequests;
case 'My Requests': return myRequests;
default: return [];
}
};
const isMyGroupsLoading = myLoading || myFetching;
const isJoinedGroupsLoading = joinedLoading || joinedFetching;
const isRequestsLoading = requestsLoading || requestsFetching;
const isUserRequestsLoading = userReqLoading || userReqFetching;
const isActiveTabLoading =
activeTab === 'My Groups'
? isMyGroupsLoading
: activeTab === 'Joined Groups'
? isJoinedGroupsLoading
: activeTab === 'Requests'
? requestSubTab === 'Join Requests'
? isRequestsLoading
: isUserRequestsLoading
: isUserRequestsLoading;
const listData = refreshing || isActiveTabLoading ? [] : getFilteredData();
return (
<AppView paddingHorizontal={0}>
<View style={styles.header}>
<View style={styles.headerTitleRow}>
<View style={{ flex: 1 }}>
<Text style={[styles.title, { color: theme.colors.textPrimary }]}>Account Hub</Text>
<Text style={[styles.subtitle, { color: theme.colors.textSecondary }]}>Manage your circles and requests</Text>
</View>
<TouchableOpacity
onPress={onRefresh}
disabled={refreshing}
style={styles.refreshBtn}
>
<LucideIcon
name="sync-outline"
size={20}
color={theme.colors.textSecondary}
style={refreshing ? { opacity: 0.5 } : {}}
/>
</TouchableOpacity>
</View>
</View>
<View style={[styles.tabContainer, { backgroundColor: theme.colors.borderLight }]}>
{tabs.map((tab) => (
<TouchableOpacity
key={tab}
onPress={() => setActiveTab(tab)}
style={[
styles.tab,
activeTab === tab && { backgroundColor: theme.colors.card, borderRadius: 12, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2 }
]}
>
<Text style={[
styles.tabText,
{ color: activeTab === tab ? theme.colors.textPrimary : theme.colors.textMuted },
activeTab === tab && { fontFamily: FontsFamily.nunitoBold }
]}>
{tab}
</Text>
</TouchableOpacity>
))}
</View>
{user.role === 'ORGANIZER' && activeTab === 'My Groups' && (
<View style={styles.statusFilterContainer}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.statusFilterRow}
>
{GROUP_STATUS_FILTERS.map((status, index) => {
const isActive = myGroupStatusFilter === status;
return (
<TouchableOpacity
key={status}
onPress={() => setMyGroupStatusFilter(status)}
style={[
styles.statusFilterChip,
index !== GROUP_STATUS_FILTERS.length - 1 && styles.statusFilterChipSpaced,
{
borderColor: isActive ? theme.colors.primary : theme.colors.border,
backgroundColor: isActive ? theme.colors.primarySoft : theme.colors.surface,
},
]}
>
<Text
style={[
styles.statusFilterText,
{
color: isActive ? theme.colors.textPrimary : theme.colors.textSecondary,
},
]}
>
{status}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
)}
{activeTab === 'Joined Groups' && (
<View style={styles.statusFilterContainer}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.statusFilterRow}
>
{JOINED_GROUP_STATUS_FILTERS.map((status, index) => {
const isActive = joinedGroupStatusFilter === status;
return (
<TouchableOpacity
key={status}
onPress={() => setJoinedGroupStatusFilter(status)}
style={[
styles.statusFilterChip,
index !== JOINED_GROUP_STATUS_FILTERS.length - 1 && styles.statusFilterChipSpaced,
{
borderColor: isActive ? theme.colors.primary : theme.colors.border,
backgroundColor: isActive ? theme.colors.primarySoft : theme.colors.surface,
},
]}
>
<Text
style={[
styles.statusFilterText,
{
color: isActive ? theme.colors.textPrimary : theme.colors.textSecondary,
},
]}
>
{status}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
)}
{activeTab === 'Requests' && user.role === 'ORGANIZER' && (
<View style={styles.statusFilterContainer}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.statusFilterRow}
>
{(['Join Requests', 'My Requests'] as RequestSubTab[]).map((tab, index) => {
const isActive = requestSubTab === tab;
return (
<TouchableOpacity
key={tab}
onPress={() => setRequestSubTab(tab)}
style={[
styles.statusFilterChip,
index !== (['Join Requests', 'My Requests'] as RequestSubTab[]).length - 1 && styles.statusFilterChipSpaced,
{
borderColor: isActive ? theme.colors.primary : theme.colors.border,
backgroundColor: isActive ? theme.colors.primarySoft : theme.colors.surface,
},
]}
>
<Text
style={[
styles.statusFilterText,
{
color: isActive ? theme.colors.textPrimary : theme.colors.textSecondary,
},
]}
>
{tab}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
)}
<FlatList
data={listData}
contentContainerStyle={styles.listContent}
keyExtractor={(item, index) => String(item.id || item.requestId || index)}
renderItem={({ item }) => {
if (activeTab === 'My Groups') return <MyGroupCard item={item} onManage={handleManageGroup} />;
if (activeTab === 'Joined Groups') return <JoinedGroupCard item={item} />;
if (activeTab === 'Requests') {
if (requestSubTab === 'Join Requests') {
return <RequestCard item={item} onApprove={handleApprove} onDecline={handleDecline} />;
} else {
return <UserRequested item={item} />;
}
}
if (activeTab === 'My Requests') return <UserRequested item={item} />;
return null;
}}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
colors={[theme.colors.primary]}
/>
}
ListEmptyComponent={
!isActiveTabLoading && !refreshing ? (
<View style={styles.emptyState}>
<LucideIcon name="documents-outline" size={64} color={theme.colors.textMuted} />
<Text style={[styles.emptyText, { color: theme.colors.textSecondary }]}>Nothing to show here</Text>
</View>
) : renderSkeletons()
}
/>
</AppView>
);
};
const styles = StyleSheet.create({
header: {
paddingHorizontal: 14,
paddingTop: 10,
paddingBottom: 12,
zIndex: 10,
},
headerTitleRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
refreshBtn: {
padding: 8,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontSize: responsiveFontSize(3.2),
fontFamily: FontsFamily.nunitoBold,
},
subtitle: {
fontSize: responsiveFontSize(1.6),
fontFamily: FontsFamily.nunitoRegular,
marginTop: 4,
},
tabContainer: {
flexDirection: 'row',
marginHorizontal: 12,
padding: 5,
borderRadius: 16,
marginBottom: 12,
zIndex: 10,
},
tab: {
flex: 1,
paddingVertical: 10,
alignItems: 'center',
justifyContent: 'center',
},
tabText: {
fontSize: responsiveFontSize(1.4),
fontFamily: FontsFamily.nunitoMedium,
},
statusFilterContainer: {
minHeight: 46,
maxHeight: 46,
marginBottom: 10,
},
statusFilterRow: {
paddingHorizontal: 12,
alignItems: 'center',
},
statusFilterChip: {
borderWidth: 1,
borderRadius: 999,
minHeight: 34,
paddingHorizontal: 14,
paddingVertical: 0,
justifyContent: 'center',
alignSelf: 'center',
},
statusFilterChipSpaced: {
marginRight: 8,
},
statusFilterText: {
fontSize: responsiveFontSize(1.25),
fontFamily: FontsFamily.nunitoSemiBold,
},
listContent: {
paddingBottom: 86,
},
skeletonContainer: {
paddingHorizontal: 12,
},
emptyState: {
alignItems: 'center',
marginTop: 64,
},
emptyText: {
marginTop: 16,
fontSize: responsiveFontSize(1.6),
fontFamily: FontsFamily.nunitoMedium,
},
});
export default ManagementScreen;