616 lines
21 KiB
TypeScript
616 lines
21 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
TouchableOpacity,
|
|
RefreshControl,
|
|
Alert,
|
|
ActivityIndicator,
|
|
TextInput,
|
|
ToastAndroid
|
|
} from 'react-native';
|
|
import { Ionicons } from '@react-native-vector-icons/ionicons';
|
|
import colors from '../../components/colors';
|
|
import DeclineModal from './DeclineModal';
|
|
import * as keychain from 'react-native-keychain'
|
|
import { BASE_URL } from '../../utils/ApiUrls';
|
|
|
|
// Define TypeScript interfaces
|
|
interface GroupRequest {
|
|
requestId: string;
|
|
groupName: string;
|
|
groupId: string;
|
|
userId: number;
|
|
userName: string;
|
|
mobile: string;
|
|
status: string;
|
|
}
|
|
|
|
interface ApiResponse {
|
|
message: string;
|
|
status: string;
|
|
statusCode: number;
|
|
data: GroupRequest[];
|
|
}
|
|
|
|
const RequestList: React.FC = () => {
|
|
const [requests, setRequests] = useState<GroupRequest[]>([]);
|
|
const [filteredRequests, setFilteredRequests] = useState<GroupRequest[]>([]);
|
|
const [refreshing, setRefreshing] = useState<boolean>(false);
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
const [processingRequest, setProcessingRequest] = useState<string | null>(null);
|
|
const [showDeclineModal, setShowDeclineModal] = useState<{ modal: boolean, requestId: string | null }>({ modal: false, requestId: null });
|
|
const [selectedRequestId, setSelectedRequestId] = useState<string | null>(null);
|
|
|
|
// Fetch data from API
|
|
const fetchRequests = async (): Promise<void> => {
|
|
|
|
const credentials = await keychain.getGenericPassword();
|
|
if (!credentials) {
|
|
ToastAndroid.show('You are not logged in', ToastAndroid.SHORT);
|
|
return;
|
|
}
|
|
const token = credentials.password;
|
|
try {
|
|
setRefreshing(true);
|
|
setError(null);
|
|
|
|
const response = await fetch(
|
|
`${BASE_URL}`,
|
|
{
|
|
method: "GET",
|
|
headers: {
|
|
accept: "*/*",
|
|
Authorization: `Bearer ${token}`
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data: ApiResponse = await response.json();
|
|
|
|
if (data.status === "success") {
|
|
setRequests(data.data);
|
|
setFilteredRequests(data.data);
|
|
} else {
|
|
throw new Error("Failed to fetch requests");
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
|
setError(errorMessage);
|
|
console.error("API Error:", error);
|
|
} finally {
|
|
setLoading(false);
|
|
setRefreshing(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
fetchRequests();
|
|
}, []);
|
|
|
|
// Filter requests based on search query
|
|
useEffect(() => {
|
|
if (searchQuery.trim() === '') {
|
|
setFilteredRequests(requests);
|
|
} else {
|
|
const query = searchQuery.toLowerCase();
|
|
const filtered = requests.filter(request =>
|
|
request.groupName.toLowerCase().includes(query) ||
|
|
request.userName.toLowerCase().includes(query) ||
|
|
request.mobile.includes(query) ||
|
|
request.status.toLowerCase().includes(query)
|
|
);
|
|
setFilteredRequests(filtered);
|
|
}
|
|
}, [searchQuery, requests]);
|
|
|
|
const handleAccept = async (requestId: string): Promise<void> => {
|
|
Alert.alert(
|
|
"Accept Request",
|
|
"Are you sure you want to accept this request?",
|
|
[
|
|
{ text: "Cancel", style: "cancel" },
|
|
{
|
|
text: "Accept",
|
|
onPress: async () => {
|
|
try {
|
|
setProcessingRequest(requestId);
|
|
|
|
// Make API call to approve the request
|
|
const response = await fetch(
|
|
`http://192.168.1.5:9060/api/fund/requests/${requestId}/approve`,
|
|
{
|
|
method: "PUT",
|
|
headers: {
|
|
"accept": "*/*",
|
|
"Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MjY1MjI5MzcxIiwidXNlcklkIjoiMTAwMDAxIiwiaWF0IjoxNzU4MDkxMjEzLCJleHAiOjE3NTgxNzc2MTN9.T3BvU7nl8fnjwwVobIWRmSFFxQzOjtyIct1Ba0i9lIU"
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
// Parse response
|
|
const data = await response.json();
|
|
|
|
if (data.status === "success") {
|
|
// Remove the accepted request from the list
|
|
const updatedRequests = requests.filter(req => req.requestId !== requestId);
|
|
setRequests(updatedRequests);
|
|
ToastAndroid.show(data.message || "Request accepted successfully", ToastAndroid.SHORT);
|
|
fetchRequests();
|
|
} else {
|
|
throw new Error(data.message || "Failed to accept request");
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
|
console.error("Accept Error:", error);
|
|
} finally {
|
|
setProcessingRequest(null);
|
|
}
|
|
}
|
|
}
|
|
]
|
|
);
|
|
};
|
|
|
|
const handleDeclineConfirm = async (requestedId: string | null, reason: string) => {
|
|
setShowDeclineModal({ modal: false, requestId: null });
|
|
try {
|
|
setProcessingRequest(requestedId);
|
|
|
|
// Make API call to decline the request
|
|
const response = await fetch(
|
|
`http://192.168.1.5:9060/api/fund/requests/${requestedId}/reject?reason=/${reason}`,
|
|
{
|
|
method: "PUT",
|
|
headers: {
|
|
"accept": "*/*",
|
|
"Authorization": "Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MjY1MjI5MzcxIiwidXNlcklkIjoiMTAwMDAxIiwiaWF0IjoxNzU4MDkxMjEzLCJleHAiOjE3NTgxNzc2MTN9.T3BvU7nl8fnjwwVobIWRmSFFxQzOjtyIct1Ba0i9lIU"
|
|
}
|
|
}
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
// Parse response
|
|
const data = await response.json();
|
|
|
|
if (data.status === "success") {
|
|
// Remove the declined request from the list
|
|
const updatedRequests = requests.filter(req => req.requestId !== selectedRequestId);
|
|
setRequests(updatedRequests);
|
|
ToastAndroid.show(data.message || "Request declined successfully", ToastAndroid.SHORT);
|
|
|
|
fetchRequests();
|
|
} else {
|
|
throw new Error(data.message || "Failed to decline request");
|
|
}
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
|
|
|
|
console.error("Decline Error:", error);
|
|
} finally {
|
|
setProcessingRequest(null);
|
|
}// call your API
|
|
};
|
|
|
|
|
|
|
|
const onRefresh = (): void => {
|
|
fetchRequests();
|
|
};
|
|
|
|
const getInitials = (name: string): string => {
|
|
return name
|
|
.split(' ')
|
|
.map(word => word.charAt(0))
|
|
.join('')
|
|
.toUpperCase()
|
|
.substring(0, 2);
|
|
};
|
|
|
|
const clearSearch = (): void => {
|
|
setSearchQuery('');
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<View style={styles.centerContainer}>
|
|
<ActivityIndicator size="large" color="#3498db" />
|
|
<Text style={styles.loadingText}>Loading requests...</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<View style={styles.centerContainer}>
|
|
<Ionicons name="alert-circle" size={64} color="#e74c3c" />
|
|
<Text style={styles.errorText}>Error loading requests</Text>
|
|
<Text style={styles.errorSubText}>{error}</Text>
|
|
<TouchableOpacity style={styles.retryButton} onPress={fetchRequests}>
|
|
<Text style={styles.retryButtonText}>Try Again</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={styles.header}>
|
|
<Text style={styles.headerTitle}>Pending Requests</Text>
|
|
<Text style={styles.headerSubtitle}>
|
|
{filteredRequests.length} pending group request{filteredRequests.length !== 1 ? 's' : ''}
|
|
{searchQuery ? ` matching "${searchQuery}"` : ''}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Search Bar */}
|
|
<View style={styles.searchContainer}>
|
|
<Ionicons name="search" size={20} color="#95a5a6" style={styles.searchIcon} />
|
|
<TextInput
|
|
style={styles.searchInput}
|
|
placeholder="Search by group, name, phone or status..."
|
|
value={searchQuery}
|
|
onChangeText={setSearchQuery}
|
|
placeholderTextColor="#95a5a6"
|
|
/>
|
|
{searchQuery ? (
|
|
<TouchableOpacity onPress={clearSearch} style={styles.clearButton}>
|
|
<Ionicons name="close-circle" size={20} color="#95a5a6" />
|
|
</TouchableOpacity>
|
|
) : null}
|
|
</View>
|
|
|
|
<ScrollView
|
|
refreshControl={
|
|
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
|
}
|
|
contentContainerStyle={filteredRequests.length === 0 ? styles.emptyScrollView : undefined}
|
|
>
|
|
{filteredRequests.length === 0 ? (
|
|
<View style={styles.emptyState}>
|
|
{searchQuery ? (
|
|
<>
|
|
<Ionicons name="search" size={64} color="#ccc" />
|
|
<Text style={styles.emptyStateText}>No matching requests</Text>
|
|
<Text style={styles.emptyStateSubtext}>
|
|
No requests found for "{searchQuery}"
|
|
</Text>
|
|
<TouchableOpacity onPress={clearSearch} style={styles.clearSearchButton}>
|
|
<Text style={styles.clearSearchText}>Clear Search</Text>
|
|
</TouchableOpacity>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Ionicons name="checkmark-done-circle" size={64} color="#ccc" />
|
|
<Text style={styles.emptyStateText}>No pending requests</Text>
|
|
<Text style={styles.emptyStateSubtext}>
|
|
You're all caught up! There are no pending group requests.
|
|
</Text>
|
|
</>
|
|
)}
|
|
</View>
|
|
) : (
|
|
filteredRequests.map((request) => (
|
|
<View key={request.requestId} style={styles.requestCard}>
|
|
<View style={styles.cardHeader}>
|
|
<View style={styles.groupInfo}>
|
|
<View style={styles.avatar}>
|
|
<Text style={styles.avatarText}>
|
|
{getInitials(request.groupName)}
|
|
</Text>
|
|
</View>
|
|
<View>
|
|
<Text style={styles.groupName}>{request.groupName}</Text>
|
|
<Text style={styles.requestText}>Group Join Request</Text>
|
|
</View>
|
|
</View>
|
|
<View style={[
|
|
styles.statusBadge,
|
|
request.status === 'PENDING' ? styles.pendingBadge : styles.otherBadge
|
|
]}>
|
|
<Text style={styles.statusText}>{request.status}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.divider} />
|
|
|
|
<View style={styles.userInfo}>
|
|
<Ionicons name="person" size={16} color="#666" />
|
|
<Text style={styles.userName}>{request.userName}</Text>
|
|
</View>
|
|
|
|
<View style={styles.userInfo}>
|
|
<Ionicons name="call" size={16} color="#666" />
|
|
<Text style={styles.userDetail}>{request.mobile}</Text>
|
|
</View>
|
|
|
|
<View style={styles.actions}>
|
|
<TouchableOpacity
|
|
style={[styles.button, styles.declineButton]}
|
|
onPress={() => setShowDeclineModal({ modal: true, requestId: request.requestId })}
|
|
disabled={processingRequest === request.requestId}
|
|
>
|
|
{processingRequest === request.requestId ? (
|
|
<ActivityIndicator size="small" color="#e74c3c" />
|
|
) : (
|
|
<Text style={styles.declineButtonText}>Decline</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.button, styles.acceptButton]}
|
|
onPress={() => handleAccept(request.requestId)}
|
|
disabled={processingRequest === request.requestId}
|
|
>
|
|
{processingRequest === request.requestId ? (
|
|
<ActivityIndicator size="small" color="#fff" />
|
|
) : (
|
|
<Text style={styles.acceptButtonText}>Accept</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
))
|
|
)}
|
|
</ScrollView>
|
|
{showDeclineModal.modal && (
|
|
<DeclineModal
|
|
visible={showDeclineModal.modal}
|
|
onClose={() => setShowDeclineModal({ modal: false, requestId: null })}
|
|
onSubmit={(reason) => handleDeclineConfirm(showDeclineModal.requestId, reason)}
|
|
/>
|
|
)}
|
|
</View>
|
|
);
|
|
};
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
paddingBottom: '100%',
|
|
},
|
|
centerContainer: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: '#f5f7fb',
|
|
padding: 20,
|
|
},
|
|
loadingText: {
|
|
fontSize: 16,
|
|
color: '#666',
|
|
marginTop: 12,
|
|
},
|
|
errorText: {
|
|
fontSize: 18,
|
|
fontWeight: '600',
|
|
color: '#e74c3c',
|
|
marginTop: 16,
|
|
marginBottom: 8,
|
|
},
|
|
errorSubText: {
|
|
fontSize: 14,
|
|
color: '#7f8c8d',
|
|
textAlign: 'center',
|
|
marginBottom: 20,
|
|
},
|
|
retryButton: {
|
|
backgroundColor: colors.pastelPink,
|
|
paddingHorizontal: 20,
|
|
paddingVertical: 10,
|
|
borderRadius: 8,
|
|
},
|
|
retryButtonText: {
|
|
color: '#fff',
|
|
fontWeight: '600',
|
|
},
|
|
header: {
|
|
// backgroundColor: '#fff',
|
|
// // padding: 20,
|
|
// borderBottomWidth: 1,
|
|
// borderBottomColor: '#eee',
|
|
// shadowColor: '#000',
|
|
// shadowOffset: { width: 0, height: 2 },
|
|
// shadowOpacity: 0.1,
|
|
// shadowRadius: 3,
|
|
// elevation: 3,
|
|
},
|
|
headerTitle: {
|
|
fontSize: 24,
|
|
fontWeight: 'bold',
|
|
color: '#2c3e50',
|
|
marginBottom: 5,
|
|
},
|
|
headerSubtitle: {
|
|
fontSize: 14,
|
|
color: '#7f8c8d',
|
|
},
|
|
// Search styles
|
|
searchContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: '#fff',
|
|
marginVertical: 16,
|
|
marginBottom: 8,
|
|
borderRadius: 10,
|
|
paddingHorizontal: 12,
|
|
height: 50,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 1 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 2,
|
|
elevation: 2,
|
|
},
|
|
searchIcon: {
|
|
marginRight: 8,
|
|
},
|
|
searchInput: {
|
|
flex: 1,
|
|
height: '100%',
|
|
fontSize: 16,
|
|
color: '#2c3e50',
|
|
padding: 0,
|
|
},
|
|
clearButton: {
|
|
padding: 4,
|
|
},
|
|
clearSearchButton: {
|
|
marginTop: 16,
|
|
paddingVertical: 8,
|
|
paddingHorizontal: 16,
|
|
backgroundColor: '#ecf0f1',
|
|
borderRadius: 6,
|
|
},
|
|
clearSearchText: {
|
|
color: '#7f8c8d',
|
|
fontWeight: '500',
|
|
},
|
|
emptyScrollView: {
|
|
flexGrow: 1,
|
|
justifyContent: 'center',
|
|
},
|
|
requestCard: {
|
|
backgroundColor: '#fff',
|
|
borderRadius: 12,
|
|
marginBottom: 16,
|
|
padding: 16,
|
|
shadowColor: '#000',
|
|
shadowOffset: { width: 0, height: 2 },
|
|
shadowOpacity: 0.1,
|
|
shadowRadius: 4,
|
|
elevation: 3,
|
|
},
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
marginBottom: 12,
|
|
},
|
|
groupInfo: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
flex: 1,
|
|
},
|
|
avatar: {
|
|
width: 50,
|
|
height: 50,
|
|
borderRadius: 25,
|
|
backgroundColor: colors.pastelPink,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
marginRight: 12,
|
|
},
|
|
avatarText: {
|
|
color: '#fff',
|
|
fontSize: 16,
|
|
fontWeight: 'bold',
|
|
},
|
|
groupName: {
|
|
fontSize: 18,
|
|
fontWeight: '600',
|
|
color: '#2c3e50',
|
|
},
|
|
requestText: {
|
|
fontSize: 14,
|
|
color: '#7f8c8d',
|
|
marginTop: 2,
|
|
},
|
|
statusBadge: {
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 5,
|
|
borderRadius: 12,
|
|
},
|
|
pendingBadge: {
|
|
backgroundColor: '#fff4e6',
|
|
},
|
|
otherBadge: {
|
|
backgroundColor: '#e8f5e9',
|
|
},
|
|
statusText: {
|
|
fontSize: 12,
|
|
fontWeight: '600',
|
|
},
|
|
divider: {
|
|
height: 1,
|
|
backgroundColor: '#ecf0f1',
|
|
marginVertical: 12,
|
|
},
|
|
userInfo: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginBottom: 8,
|
|
},
|
|
userName: {
|
|
fontSize: 16,
|
|
color: '#2c3e50',
|
|
marginLeft: 8,
|
|
},
|
|
userDetail: {
|
|
fontSize: 14,
|
|
color: '#7f8c8d',
|
|
marginLeft: 8,
|
|
},
|
|
actions: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'flex-end',
|
|
marginTop: 16,
|
|
},
|
|
button: {
|
|
paddingVertical: 10,
|
|
paddingHorizontal: 20,
|
|
borderRadius: 8,
|
|
marginLeft: 12,
|
|
minWidth: 100,
|
|
alignItems: 'center',
|
|
},
|
|
acceptButton: {
|
|
backgroundColor: '#2ecc71',
|
|
},
|
|
declineButton: {
|
|
backgroundColor: '#fff',
|
|
borderWidth: 1,
|
|
borderColor: '#e74c3c',
|
|
},
|
|
acceptButtonText: {
|
|
color: '#fff',
|
|
fontWeight: '600',
|
|
},
|
|
declineButtonText: {
|
|
color: '#e74c3c',
|
|
fontWeight: '600',
|
|
},
|
|
emptyState: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: 40,
|
|
},
|
|
emptyStateText: {
|
|
fontSize: 18,
|
|
fontWeight: '600',
|
|
color: '#7f8c8d',
|
|
marginTop: 16,
|
|
marginBottom: 8,
|
|
},
|
|
emptyStateSubtext: {
|
|
fontSize: 14,
|
|
color: '#bdc3c7',
|
|
textAlign: 'center',
|
|
},
|
|
});
|
|
|
|
export default RequestList; |