theme update latest project

This commit is contained in:
Abin Toppo
2026-02-16 10:41:38 +05:30
parent bd7dbd864c
commit a98e0b6cdd
75 changed files with 10173 additions and 7915 deletions
+347 -125
View File
@@ -1,178 +1,400 @@
// src/screens/ManagementScreen.tsx
import React, { useState, useCallback, FC, useEffect } from "react";
import React, { useState, useMemo, useEffect, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
ActivityIndicator,
Alert,
ScrollView,
ToastAndroid,
} 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';
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";
import { RootStackParamList } from "../../navigation/AppNavigator";
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 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';
type GroupStatusFilter = 'PENDING' | 'APPROVED' | 'ACTIVE' | 'DELAY' | 'COMPLETED' | 'REJECTED' | 'CANCELLED';
const ManagementScreen: FC = () => {
const GROUP_STATUS_FILTERS: GroupStatusFilter[] = [
'PENDING',
'APPROVED',
'ACTIVE',
'DELAY',
'COMPLETED',
'REJECTED',
'CANCELLED',
];
const JOINED_GROUP_STATUS_FILTERS: GroupStatusFilter[] = [
'ACTIVE',
'DELAY',
'COMPLETED',
];
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<GroupManagementNavProp>();
const navigation = useNavigation<any>();
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);
const [activeTab, setActiveTab] = useState<ActiveTab>(
user.role === 'ORGANIZER' ? 'My Groups' : 'Joined Groups'
);
const [myGroupStatusFilter, setMyGroupStatusFilter] = useState<GroupStatusFilter>('APPROVED');
const [joinedGroupStatusFilter, setJoinedGroupStatusFilter] = useState<GroupStatusFilter>('ACTIVE');
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]);
if (!route.params?.actionTab) return;
const routeTab = route.params.actionTab as ActiveTab;
setActiveTab(routeTab);
}, [route.params?.actionTab]);
const myGroupsParams = useMemo(
() => ({ status: myGroupStatusFilter, page: 0, size: 10 }),
[myGroupStatusFilter]
);
const fetchData = useCallback(async () => {
const joinedGroupsParams = useMemo(
() => ({ status: joinedGroupStatusFilter, page: 0, size: 10 }),
[joinedGroupStatusFilter]
);
if (loading) return;
// 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,
undefined,
{ enabled: user.role === 'ORGANIZER' }
);
const { data: userReqRes, isLoading: userReqLoading, refetch: refetchUserReq } = useFetch<any>(
USER_REQUEST,
undefined,
{ enabled: user.role !== 'ORGANIZER' }
);
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[]>>,
]);
console.log("GET_JOINED_GROUP_URL",joinedGroupsRes)
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);
}
const { mutate: updateRequest } = useUpdate<any, any>('');
}, [user.role]);
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(() => {
fetchData();
}, [fetchData])
onRefresh();
}, [])
);
const handleViewGroup = (item: JoinedGroup) => {
const tabs: ActiveTab[] = useMemo(() =>
user.role === 'ORGANIZER'
? ['My Groups', 'Joined Groups', 'Requests']
: ['Joined Groups', 'Your 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 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) {
ToastAndroid.show("Success", ToastAndroid.SHORT);
fetchData(); // Refresh data after action
} else {
throw new Error(res?.message || failureMessage);
}
} catch (err: any) {
console.error("Action Error:", err);
ToastAndroid.show(err.message || failureMessage, 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 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 });
const renderContent = () => {
if (error) return <Text style={styles.emptyText}>{error}</Text>;
if (loading && myGroups.length === 0 && joinedGroups.length === 0 && requests.length === 0) {
return <ActivityIndicator size="large" color={colors.primary} style={styles.loader} />;
}
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 <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;
case 'My Groups': return myGroups;
case 'Joined Groups': return joinedGroups;
case 'Requests': return incomingRequests;
case 'Your Requests': return myRequests;
default: return [];
}
};
const tabs: ActiveTab[] = user.role === 'ORGANIZER' ? ["My Groups", "Joined Groups", "Requests"] : ["Joined Groups", "Your Requests"];
const isLoading = myLoading || joinedLoading || requestsLoading || userReqLoading;
return (
<AppView style={styles.container} paddingHorizontal={0}>
<AppView paddingHorizontal={0}>
<View style={styles.header}>
<Text style={styles.title}>Group Management</Text>
<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>
<View style={styles.segmentedControl}>
<View style={[styles.tabContainer, { backgroundColor: theme.colors.borderLight }]}>
{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
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>
<View style={styles.contentArea}>
{renderContent()}
</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>
)}
<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') return <RequestCard item={item} onApprove={handleApprove} onDecline={handleDecline} />;
if (activeTab === 'Your 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({
container: { flex: 1, backgroundColor: '#f5f6fa' },
header: { paddingHorizontal: 20, paddingBottom: 15, paddingTop: 10 },
title: { fontSize: 22, fontWeight: "600"},
segmentedControl: { flexDirection: 'row',borderRadius: 10,marginBottom: 20, padding: 3 },
segmentButton: { flex: 1, paddingVertical: 12, alignItems: 'center', borderRadius: 8 },
segmentActive: { backgroundColor: colors.pastelOrange, elevation: 5,borderRadius:15, shadowColor: '#000', shadowOpacity: 0.1, shadowRadius: 5 },
segmentText: { fontSize: 14, fontWeight: '600', color: '#6c757d' },
segmentTextActive: { color: colors.textDark },
contentArea: { flex: 1},
loader: { marginTop: 50 },
emptyText: { textAlign: 'center', marginTop: 60, fontSize: 16, color: '#6c757d' },
header: {
paddingHorizontal: 14,
paddingTop: 10,
paddingBottom: 12,
zIndex: 10,
},
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;