496 lines
16 KiB
TypeScript
496 lines
16 KiB
TypeScript
import React, { useState, useMemo, 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 = useMemo(() => {
|
|
return {
|
|
...(myGroupStatusFilter && myGroupStatusFilter !== 'ALL'
|
|
? { status: myGroupStatusFilter }
|
|
: {}),
|
|
page: 0,
|
|
size: 10,
|
|
};
|
|
}, [myGroupStatusFilter]);
|
|
|
|
const joinedGroupsParams = useMemo(() => {
|
|
return {
|
|
...(joinedGroupStatusFilter && joinedGroupStatusFilter !== 'ALL'
|
|
? { status: joinedGroupStatusFilter }
|
|
: {}),
|
|
page: 0,
|
|
size: 10,
|
|
};
|
|
}, [joinedGroupStatusFilter]);
|
|
|
|
const requestsParams = useMemo(() => ({ page: 0, size: 10 }), []);
|
|
const myRequestsParams = useMemo(() => ({ page: 0, size: 10 }), []);
|
|
|
|
console.log('joinedGroupsParams:', joinedGroupsParams);
|
|
|
|
|
|
// Data Fetching
|
|
const { data: myGroupsRes, isLoading: myLoading, refetch: refetchMy } = useFetch<any>(
|
|
MY_GROUPS_URL,
|
|
myGroupsParams,
|
|
{ enabled: user.role === 'ORGANIZER' }
|
|
);
|
|
const { data: joinedRes, isLoading: joinedLoading, refetch: refetchJoined } = useFetch<any>(
|
|
GET_JOINED_GROUP_URL,
|
|
joinedGroupsParams
|
|
);
|
|
const { data: requestsRes, isLoading: requestsLoading, refetch: refetchRequests } = useFetch<any>(
|
|
REQUESTS_URL,
|
|
requestsParams,
|
|
{ enabled: user.role === 'ORGANIZER' }
|
|
);
|
|
const { data: userReqRes, isLoading: userReqLoading, refetch: refetchUserReq } = useFetch<any>(
|
|
USER_REQUEST,
|
|
myRequestsParams
|
|
);
|
|
|
|
const { mutate: updateRequest } = useUpdate<any, any>('');
|
|
|
|
const myGroups = useMemo(() => getListFromResponse<MyGroup>(myGroupsRes), [myGroupsRes]);
|
|
const joinedGroups = useMemo(() => getListFromResponse<any>(joinedRes), [joinedRes]);
|
|
const incomingRequests = useMemo(() => getListFromResponse<JoinRequest>(requestsRes), [requestsRes]);
|
|
const myRequests = useMemo(() => getListFromResponse<any>(userReqRes), [userReqRes]);
|
|
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const onRefresh = useCallback(() => {
|
|
setRefreshing(true);
|
|
Promise.all([
|
|
refetchMy(),
|
|
refetchJoined(),
|
|
refetchRequests(),
|
|
refetchUserReq(),
|
|
]).finally(() => {
|
|
setRefreshing(false);
|
|
});
|
|
}, [refetchMy, refetchJoined, refetchRequests, refetchUserReq]);
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
onRefresh();
|
|
}, [])
|
|
);
|
|
|
|
const tabs: ActiveTab[] = useMemo(() =>
|
|
user.role === 'ORGANIZER'
|
|
? ['My Groups', 'Joined Groups', 'Requests']
|
|
: ['Joined Groups', 'My Requests'],
|
|
[user.role]);
|
|
|
|
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=/`, 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 isLoading = myLoading || joinedLoading || requestsLoading || userReqLoading;
|
|
|
|
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={getFilteredData()}
|
|
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={
|
|
!isLoading ? (
|
|
<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;
|