payment cycle implementation
This commit is contained in:
@@ -19,6 +19,7 @@ import ManageJoinedGroup from "../screens/management/ManageJoinedGroup";
|
||||
import InviteScreen from "../screens/InviteScreen";
|
||||
import ManagementTabNavigation from "../screens/management/ManagementTabNavigation";
|
||||
import TransactionsList from "../screens/transaction/TransactionList";
|
||||
import RandomSelectUserScreen from "../screens/management/RandomSelectUserScreen";
|
||||
|
||||
export type RootStackParamList = {
|
||||
Splash: undefined;
|
||||
@@ -40,6 +41,7 @@ export type RootStackParamList = {
|
||||
Invite: { groupId: string | number; code: string | number };
|
||||
ManagementTabs:{ groupId: string | number;}
|
||||
TransactionList:undefined
|
||||
RandomSelectUserScreen: { groupId: string | number, cycleNumber: number };
|
||||
};
|
||||
|
||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||
@@ -92,6 +94,7 @@ const linking = {
|
||||
<Stack.Screen name="Invite" component={InviteScreen} options={{ headerShown: false }} />
|
||||
<Stack.Screen name="ManagementTabs" component={ManagementTabNavigation} options={{ headerShown: false }} />
|
||||
<Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} />
|
||||
<Stack.Screen name="RandomSelectUserScreen" component={RandomSelectUserScreen} options={{ headerShown: false }} />
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
|
||||
@@ -79,8 +79,6 @@ const GroupCard = ({ item, onJoin, onView, isGrid }: { item: Group; onJoin: (gro
|
||||
};
|
||||
|
||||
const FeaturedGroupCard = ({ item }: { item: Group }) => {
|
||||
console.log("image item : ", item)
|
||||
console.log("image view : ", `data:image/jpeg;base64${item?.imageBlob}`)
|
||||
return (
|
||||
<View style={styles.featuredCard}>
|
||||
<Image source={{ uri: item?.imageBlob !== null ? `data:image/jpeg;base64${item?.imageBlob}` : `https://i.pravatar.cc/150?u=${item.id}` }} style={styles.featuredImage} />
|
||||
|
||||
@@ -222,7 +222,7 @@ const Home: React.FC = () => {
|
||||
return (
|
||||
<AppView barStyle="dark-content" style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.greeting}>Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}👋</Text>
|
||||
<Text style={styles.greeting}>Hello, {user.firstName || apiUser?.firstName} {user.lastName || apiUser?.lastName}</Text>
|
||||
<TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}>
|
||||
<Image
|
||||
source={
|
||||
@@ -270,7 +270,7 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
justifyContent: "space-around",
|
||||
},
|
||||
greeting: { fontSize: 22, fontWeight: "600", color: colors.Home },
|
||||
greeting: { fontSize: 22, fontWeight: "600", color: colors.pastelPink },
|
||||
gridButton: {
|
||||
flex: 1,
|
||||
margin: 8,
|
||||
|
||||
@@ -110,7 +110,8 @@ const UserProfileStatus: React.FC = () => {
|
||||
{/* <Text style={styles.title}>Welcome Back 👋</Text> */}
|
||||
|
||||
<LinearGradient
|
||||
colors={[colors.primary, colors.Home]}
|
||||
colors={[colors.pastelPink, colors.pastelPink
|
||||
]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
style={styles.card}
|
||||
|
||||
@@ -19,7 +19,7 @@ import AppView from '../../components/AppView';
|
||||
import { Picker } from '@react-native-picker/picker';
|
||||
import colors from '../../components/colors';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { CompositeScreenProps } from '@react-navigation/native';
|
||||
import { CompositeScreenProps, useNavigation } from '@react-navigation/native';
|
||||
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
|
||||
import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path
|
||||
import { UPLOAD_GROUP_IMAGE } from '../../utils/ApiUrls';
|
||||
@@ -27,6 +27,8 @@ import { launchImageLibrary } from 'react-native-image-picker';
|
||||
import *as Keychain from 'react-native-keychain'
|
||||
import Ionicons from '@react-native-vector-icons/ionicons';
|
||||
import { CustomButton } from '../../components/common/Custom';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
|
||||
|
||||
// --- Type Definitions ---
|
||||
interface Participant {
|
||||
@@ -37,6 +39,7 @@ interface Participant {
|
||||
role: 'ORGANIZER' | 'USER';
|
||||
}
|
||||
|
||||
|
||||
export interface GroupDetails {
|
||||
id: string;
|
||||
groupName: string;
|
||||
@@ -61,6 +64,7 @@ export interface GroupDetails {
|
||||
imageBlob: string
|
||||
}
|
||||
|
||||
|
||||
export interface EditGroupPayload {
|
||||
groupName: string;
|
||||
description: string;
|
||||
@@ -74,17 +78,21 @@ export interface EditGroupPayload {
|
||||
lateFeeValue: number | null;
|
||||
}
|
||||
|
||||
|
||||
// Correct props for a screen inside a nested Tab Navigator
|
||||
type EditGroupScreenProps = CompositeScreenProps<
|
||||
BottomTabScreenProps<ManagementTabParamList, 'EditGroup'>,
|
||||
NativeStackScreenProps<RootStackParamList>
|
||||
>;
|
||||
|
||||
|
||||
type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null;
|
||||
|
||||
type GroupManagementNavProp = StackNavigationProp<RootStackParamList, 'EditGroup'>;
|
||||
// --- Main Screen Component ---
|
||||
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
const { groupId } = route.params;
|
||||
const navigation = useNavigation<GroupManagementNavProp>();
|
||||
|
||||
const [editingCard, setEditingCard] = useState<EditingCard>(null);
|
||||
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
|
||||
@@ -106,10 +114,11 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [inviting, setInviting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [paymentStatus, setPaymentStatus] = useState<boolean | null>(null);
|
||||
const [currentCycle, setCurrentCycle] = useState<number | null>(null);
|
||||
const [selectedImage, setSelectedImage] = useState<string | null>()
|
||||
const [imageUpdating, setImageUpdating] = useState<boolean>(false)
|
||||
|
||||
// ... (All your handler functions: initializeEditableData, fetchGroupDetails, handleCancel, handleSave, etc. remain the same)
|
||||
const initializeEditableData = (data: GroupDetails) => {
|
||||
setEditableData({
|
||||
groupName: data.groupName,
|
||||
@@ -125,6 +134,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const fetchGroupDetails = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -143,11 +153,22 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [groupId]);
|
||||
const getPaymentStatus = async()=>{
|
||||
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/group/${groupId}/payment/paid`, 'GET') as { status: string; data?: { currentCycle: number ,status: boolean }; message?: string };
|
||||
console.log("paymentStatus",response)
|
||||
if (response.status === 'success') {
|
||||
setPaymentStatus(response.data?.status || false);
|
||||
setCurrentCycle(response.data?.currentCycle || null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroupDetails();
|
||||
getPaymentStatus();
|
||||
}, [fetchGroupDetails]);
|
||||
|
||||
|
||||
const handleCancel = () => {
|
||||
if (groupDetails) {
|
||||
initializeEditableData(groupDetails);
|
||||
@@ -155,9 +176,11 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
setEditingCard(null);
|
||||
};
|
||||
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!groupDetails) return;
|
||||
|
||||
|
||||
if (editableData.maxParticipants < groupDetails.totalParticipants) {
|
||||
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
|
||||
return;
|
||||
@@ -175,10 +198,14 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
const handleDistribute = async () => {
|
||||
navigation.navigate('RandomSelectUserScreen', { groupId: groupDetails?.id || '', cycleNumber: currentCycle || 0 });
|
||||
};
|
||||
|
||||
const handleStartGroup = async () => {
|
||||
if (!groupDetails) return;
|
||||
|
||||
|
||||
Alert.alert(
|
||||
"Confirm Start",
|
||||
"Are you sure you want to start this group? This action cannot be undone.",
|
||||
@@ -208,7 +235,8 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
if (!groupDetails) return;
|
||||
setInviting(true);
|
||||
try {
|
||||
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
|
||||
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/invite/generate?groupId=${groupDetails.id}`, 'GET') as { data?: string };
|
||||
|
||||
|
||||
const inviteLink = response.data;
|
||||
|
||||
@@ -230,12 +258,13 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
|
||||
setEditableData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// ... (renderField and renderCard functions also remain the same)
|
||||
const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean) => {
|
||||
|
||||
const renderField = (field: keyof EditGroupPayload, label: string, displayValue: string | number, isEditing: boolean, isEditable: boolean) => {
|
||||
const commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
|
||||
|
||||
const getInputForField = () => {
|
||||
@@ -286,6 +315,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
const renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => {
|
||||
const isEditingThisCard = editingCard === cardKey;
|
||||
|
||||
|
||||
return (
|
||||
<View style={styles.card}>
|
||||
<View style={styles.cardHeader}>
|
||||
@@ -319,6 +349,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
if (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
|
||||
if (!groupDetails) return null;
|
||||
|
||||
|
||||
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
|
||||
const isInvitable = groupDetails.status === 'APPROVED';
|
||||
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
|
||||
@@ -447,7 +478,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
|
||||
|
||||
return (
|
||||
// Use AppView as the root container with flex: 1
|
||||
<AppView style={{ flex: 1, paddingHorizontal: 0 }}>
|
||||
<View style={styles.headerContainer}>
|
||||
<Text style={styles.headerTitle}>Manage Group</Text>
|
||||
@@ -458,6 +488,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
)}
|
||||
</View>
|
||||
|
||||
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
@@ -490,20 +521,29 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
{ field: 'description', label: 'Description', displayValue: () => groupDetails.description },
|
||||
], 'groupInfo')}
|
||||
|
||||
{renderCard('Financial Rules', [
|
||||
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
|
||||
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
|
||||
{ field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
|
||||
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
|
||||
], 'financialRules')}
|
||||
|
||||
{groupDetails.status === 'ACTIVE' ?
|
||||
renderCard('Financial Rules', [
|
||||
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
|
||||
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
|
||||
{ field: 'maxParticipants', label: 'Current Participants', displayValue: () => `${groupDetails.totalParticipants}` },
|
||||
], 'financialRules')
|
||||
:
|
||||
renderCard('Financial Rules', [
|
||||
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
|
||||
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` },
|
||||
{ field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` },
|
||||
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
|
||||
], 'financialRules')
|
||||
}
|
||||
|
||||
|
||||
{renderCard('Configuration', [
|
||||
{ field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType },
|
||||
{ field: 'customCycleDays', label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays ?? ''} days` },
|
||||
...(groupDetails.cycleType === 'CUSTOM' ? [{ field: 'customCycleDays' as keyof EditGroupPayload, label: 'Custom Days', displayValue: () => `${groupDetails.customCycleDays} days` }] : []),
|
||||
{ field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility },
|
||||
], 'configuration')}
|
||||
|
||||
{/* Participants Card ... */}
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
|
||||
{groupDetails.participants.length > 0 ? (
|
||||
@@ -524,8 +564,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* --- FLOATING BUTTON AREA --- */}
|
||||
{/* This View is a sibling of ScrollView, allowing it to float on top */}
|
||||
|
||||
{canStartGroup && (
|
||||
<View style={styles.floatingButtonContainer}>
|
||||
<TouchableOpacity
|
||||
@@ -541,14 +580,28 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
{paymentStatus && (
|
||||
<View style={styles.floatingButtonContainer}>
|
||||
<TouchableOpacity
|
||||
style={styles.activateButton}
|
||||
onPress={handleDistribute}
|
||||
disabled={starting}
|
||||
>
|
||||
{starting ? (
|
||||
<ActivityIndicator color="#FFFFFF" size="small" />
|
||||
) : (
|
||||
<Text style={styles.activateButtonText}>Distribute</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
</AppView>
|
||||
);
|
||||
};
|
||||
|
||||
// --- Styles ---
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#F3F4F6' },
|
||||
// Added more paddingBottom to ensure content doesn't hide behind the button AND the tab bar
|
||||
contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 },
|
||||
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
||||
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
|
||||
@@ -619,15 +672,13 @@ const styles = StyleSheet.create({
|
||||
cancelButton: { backgroundColor: '#E5E7EB' },
|
||||
inviteButton: { backgroundColor: colors.primary },
|
||||
buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' },
|
||||
|
||||
// --- FLOATING BUTTON STYLES ---
|
||||
floatingButtonContainer: {
|
||||
position: 'absolute',
|
||||
bottom: 85, // Positioned above the tab bar
|
||||
bottom: 85,
|
||||
left: 20,
|
||||
right: 20,
|
||||
alignItems: 'center', // Center the button horizontally
|
||||
zIndex: 1, // Ensure it's on top
|
||||
alignItems: 'center',
|
||||
zIndex: 1,
|
||||
},
|
||||
activateButton: {
|
||||
backgroundColor: colors.pastelOrange,
|
||||
@@ -639,7 +690,7 @@ const styles = StyleSheet.create({
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
width: '90%', // Adjust width as needed
|
||||
width: '90%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
activateButtonText: {
|
||||
@@ -649,4 +700,5 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
export default EditGroupScreen;
|
||||
|
||||
+159
-196
@@ -1,15 +1,15 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity } from 'react-native';
|
||||
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity, Modal, TextInput } from 'react-native';
|
||||
import { useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
||||
import colors from '../../components/colors';
|
||||
import AppView from '../../components/AppView';
|
||||
import { apiCall } from '../../utils/ApiHelper';
|
||||
|
||||
// --- TYPE DEFINITIONS ---
|
||||
// --- CONSTANTS ---
|
||||
const API_BASE_URL = 'https://exotic-humpback-mistakenly.ngrok-free.app/api/payments';
|
||||
|
||||
// Generic API Response Wrapper
|
||||
// --- TYPE DEFINITIONS ---
|
||||
interface ApiResponse<T> {
|
||||
status: 'success' | 'error';
|
||||
data: T;
|
||||
@@ -17,69 +17,54 @@ interface ApiResponse<T> {
|
||||
statusCode: number;
|
||||
}
|
||||
|
||||
// Type for an individual participant in the payment details
|
||||
interface Participant {
|
||||
userId: string;
|
||||
userName: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
mobile: string;
|
||||
contributionAmount: number;
|
||||
paidAmount: number;
|
||||
payAmount?: number;
|
||||
status: 'SUCCESS' | 'PENDING';
|
||||
lateFee: number;
|
||||
daysLate: number;
|
||||
lateFeeWaived: boolean;
|
||||
}
|
||||
|
||||
// Fully typed payment details based on your JSON response
|
||||
interface PaymentDetails {
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
currentCycle: number;
|
||||
cycleType: string;
|
||||
contributionAmount: number;
|
||||
totalContributedAmount: number;
|
||||
lateFeeMode: string;
|
||||
lateFeeValue: number;
|
||||
notReceivedAmount: number;
|
||||
totalLateFee: number;
|
||||
totalDueWithLateFee: number;
|
||||
participants: Participant[];
|
||||
}
|
||||
|
||||
// Assuming your navigation stack params look something like this
|
||||
type PaymentStackParamList = {
|
||||
Payments: { groupId: string };
|
||||
};
|
||||
|
||||
type PaymentStackParamList = { Payments: { groupId: string } };
|
||||
type PaymentsRouteProp = RouteProp<PaymentStackParamList, 'Payments'>;
|
||||
type PaymentsNavigationProp = StackNavigationProp<PaymentStackParamList>;
|
||||
|
||||
const Payments: React.FC = () => {
|
||||
const route = useRoute<PaymentsRouteProp>();
|
||||
const navigation = useNavigation<PaymentsNavigationProp>();
|
||||
const { groupId } = route.params;
|
||||
|
||||
const [paymentDetails, setPaymentDetails] = useState<PaymentDetails | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// State for the waiver modal
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
const [currentParticipant, setCurrentParticipant] = useState<Participant | null>(null);
|
||||
const [waiverAmount, setWaiverAmount] = useState('');
|
||||
|
||||
const fetchPaymentDetails = useCallback(async () => {
|
||||
if (!refreshing) setLoading(true);
|
||||
try {
|
||||
const response = await apiCall(
|
||||
`https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/current/payment/details/${groupId}`,
|
||||
'GET'
|
||||
) as ApiResponse<PaymentDetails>;
|
||||
|
||||
const response = await apiCall(`${API_BASE_URL}/current/payment/details/${groupId}`, 'GET') as ApiResponse<PaymentDetails>;
|
||||
if (response && response.statusCode === 200) {
|
||||
setPaymentDetails(response.data);
|
||||
} else {
|
||||
Alert.alert("Error", response?.message || "Failed to fetch payment details.");
|
||||
Alert.alert("Error", response?.message || "Failed to fetch details.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Fetch payment details error:", error);
|
||||
Alert.alert("Error", error.message || "An unexpected error occurred.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -91,205 +76,183 @@ const Payments: React.FC = () => {
|
||||
fetchPaymentDetails();
|
||||
}, [fetchPaymentDetails]);
|
||||
|
||||
const onRefresh = () => {
|
||||
const onRefresh = useCallback(() => {
|
||||
setRefreshing(true);
|
||||
fetchPaymentDetails();
|
||||
}, [fetchPaymentDetails]);
|
||||
|
||||
const openWaiverDialog = (participant: Participant) => {
|
||||
setCurrentParticipant(participant);
|
||||
setWaiverAmount('');
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
// --- UPDATED: handleWaiveFee now uses the correct API endpoint ---
|
||||
const handleWaiveFee = async () => {
|
||||
if (!currentParticipant || !paymentDetails) return;
|
||||
|
||||
const amountToWaive = parseFloat(waiverAmount);
|
||||
|
||||
if (isNaN(amountToWaive) || amountToWaive <= 0) {
|
||||
Alert.alert("Invalid Input", "Please enter a waiver amount greater than zero.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (amountToWaive > currentParticipant.lateFee) {
|
||||
Alert.alert("Invalid Amount", `Waiver cannot exceed the current late fee of ₹${currentParticipant.lateFee}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Construct the exact URL as provided
|
||||
const url = `${API_BASE_URL}/groups/${groupId}/cycles/${paymentDetails.currentCycle}/waive/${currentParticipant.userId}?amountToWaive=${amountToWaive}`;
|
||||
|
||||
const response = await apiCall(url, 'PUT', {}); // Body is empty as amount is in URL
|
||||
|
||||
if (response && response.statusCode === 200) {
|
||||
Alert.alert("Success", `₹${amountToWaive} has been waived successfully.`);
|
||||
setModalVisible(false);
|
||||
onRefresh(); // Refresh data to reflect the change
|
||||
} else {
|
||||
throw new Error(response?.message || "Failed to apply waiver.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
Alert.alert("API Error", error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// --- ENHANCED: renderParticipant now shows late fee details ---
|
||||
const renderParticipant = (participant: Participant) => {
|
||||
const isPending = participant.status === 'PENDING';
|
||||
const canWaive = isPending && participant.lateFee > 0;
|
||||
|
||||
return (
|
||||
<View key={participant.userId} style={styles.participantCard}>
|
||||
<View style={styles.participantInfo}>
|
||||
<Text style={styles.participantName}>{participant.firstName} {participant.lastName}</Text>
|
||||
<Text style={styles.participantMobile}>{participant.mobile}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.paymentStatus}>
|
||||
<Text style={[
|
||||
styles.statusText,
|
||||
{ color: isPending ? '#e0d626ff' : '#18b107ff' }
|
||||
]}>
|
||||
<Text style={[styles.statusText, { color: isPending ? colors.warning : colors.success }]}>
|
||||
{participant.status}
|
||||
</Text>
|
||||
<Text style={styles.amountText}>
|
||||
{isPending ?`₹${participant.payAmount}`:`₹${participant.paidAmount}`}
|
||||
{isPending ? `₹${participant.payAmount || 0}` : `₹${participant.paidAmount}`}
|
||||
</Text>
|
||||
|
||||
{/* --- NEW: Conditionally render late payment stats --- */}
|
||||
{isPending && participant.daysLate > 0 && (
|
||||
<View style={styles.lateFeeContainer}>
|
||||
<Text style={styles.lateFeeText}>Late Fee: ₹{participant.lateFee}</Text>
|
||||
<Text style={styles.lateFeeText}>Days Late: {participant.daysLate}</Text>
|
||||
</View>
|
||||
{isPending && participant.lateFee > 0 && (
|
||||
<Text style={styles.lateFeeText}>Late Fee: ₹{participant.lateFee}</Text>
|
||||
)}
|
||||
{participant.lateFee === 0 && participant.lateFeeWaived && <Text style={styles.waivedText}>Fee Waived</Text>}
|
||||
</View>
|
||||
|
||||
<View style={styles.menuContainer}>
|
||||
{canWaive && (
|
||||
<TouchableOpacity onPress={() => openWaiverDialog(participant)}>
|
||||
<Ionicons name="ellipsis-vertical" size={22} color="#6c757d" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading && !refreshing) {
|
||||
return <ActivityIndicator style={styles.centered} size="large" color={colors.primary} />;
|
||||
}
|
||||
|
||||
if (!paymentDetails) {
|
||||
return (
|
||||
<View style={styles.centered}>
|
||||
<Text>No payment details found.</Text>
|
||||
<TouchableOpacity onPress={fetchPaymentDetails} style={styles.retryButton}>
|
||||
<Text style={styles.retryButtonText}>Tap to retry</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<AppView paddingHorizontal={0}>
|
||||
<Modal
|
||||
transparent={true}
|
||||
visible={isModalVisible}
|
||||
animationType="fade"
|
||||
onRequestClose={() => setModalVisible(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContainer}>
|
||||
<Text style={styles.modalTitle}>Waive Late Fee</Text>
|
||||
<Text style={styles.modalSubtitle}>
|
||||
For {currentParticipant?.firstName}. Current late fee is ₹{currentParticipant?.lateFee}.
|
||||
</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
keyboardType="numeric"
|
||||
value={waiverAmount}
|
||||
onChangeText={setWaiverAmount}
|
||||
placeholder="Enter amount to waive"
|
||||
/>
|
||||
<View style={styles.modalButtonContainer}>
|
||||
<TouchableOpacity style={[styles.modalButton, styles.cancelButton]} onPress={() => setModalVisible(false)}>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={[styles.modalButton, styles.saveButton]} onPress={handleWaiveFee}>
|
||||
<Text style={styles.saveButtonText}>Apply Waiver</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} colors={[colors.primary]} tintColor={colors.primary} />}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>{paymentDetails.groupName}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.summaryContainer}>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Current Cycle</Text>
|
||||
<Text style={styles.summaryValue}>{paymentDetails.currentCycle}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Contribution</Text>
|
||||
<Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Total Due</Text>
|
||||
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails.totalDueWithLateFee}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionTitle}>Participants</Text>
|
||||
{paymentDetails.participants.map(renderParticipant)}
|
||||
{loading && !refreshing ? (
|
||||
<ActivityIndicator style={{marginTop: 50}} size="large" color={colors.primary} />
|
||||
) : (
|
||||
<>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>{paymentDetails?.groupName}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryContainer}>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Cycle</Text>
|
||||
<Text style={styles.summaryValue}>{paymentDetails?.currentCycle}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Contribution</Text>
|
||||
<Text style={styles.summaryValue}>₹{paymentDetails?.contributionAmount}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryBox}>
|
||||
<Text style={styles.summaryLabel}>Total Due</Text>
|
||||
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails?.totalDueWithLateFee}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.sectionTitle}>Participants</Text>
|
||||
{paymentDetails?.participants.map(renderParticipant)}
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</AppView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
centered: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f4f6f8',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f4f6f8',
|
||||
},
|
||||
header: {
|
||||
padding: 20,
|
||||
paddingBottom: 15,
|
||||
},
|
||||
headerTitle: {
|
||||
color: '#000000',
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
summaryContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
padding: 20,
|
||||
backgroundColor: '#fff',
|
||||
marginHorizontal: 15,
|
||||
borderRadius: 12,
|
||||
elevation: 2,
|
||||
shadowColor: '#000',
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 10,
|
||||
},
|
||||
summaryBox: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
summaryLabel: {
|
||||
fontSize: 14,
|
||||
color: '#6c757d',
|
||||
marginBottom: 4,
|
||||
},
|
||||
summaryValue: {
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
color: colors.primary,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 20,
|
||||
marginTop: 20,
|
||||
marginBottom: 10,
|
||||
color: '#343a40',
|
||||
},
|
||||
participantCard: {
|
||||
backgroundColor: '#fff',
|
||||
marginHorizontal: 15,
|
||||
marginBottom: 10,
|
||||
borderRadius: 10,
|
||||
padding: 15,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
elevation: 1,
|
||||
},
|
||||
participantInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
participantName: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#212529',
|
||||
},
|
||||
participantMobile: {
|
||||
fontSize: 14,
|
||||
color: '#6c757d',
|
||||
marginTop: 2,
|
||||
},
|
||||
paymentStatus: {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
statusText: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
amountText: {
|
||||
fontSize: 14,
|
||||
color: '#495057',
|
||||
marginTop: 2,
|
||||
},
|
||||
// --- NEW STYLES ---
|
||||
lateFeeContainer: {
|
||||
marginTop: 8,
|
||||
paddingTop: 5,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#e9ecef',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
lateFeeText: {
|
||||
fontSize: 13,
|
||||
color: colors.danger,
|
||||
fontWeight: '600',
|
||||
},
|
||||
retryButton: {
|
||||
marginTop: 20,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 8,
|
||||
backgroundColor: colors.primary,
|
||||
},
|
||||
retryButtonText: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
container: { flex: 1, backgroundColor: '#f4f6f8' },
|
||||
header: { padding: 20, paddingBottom: 15 },
|
||||
headerTitle: { color: '#000000', fontSize: 24, fontWeight: 'bold' },
|
||||
summaryContainer: { flexDirection: 'row', justifyContent: 'space-around', padding: 20, backgroundColor: '#fff', marginHorizontal: 15, borderRadius: 12, elevation: 2 },
|
||||
summaryBox: { alignItems: 'center' },
|
||||
summaryLabel: { fontSize: 14, color: '#6c757d', marginBottom: 4 },
|
||||
summaryValue: { fontSize: 18, fontWeight: '700', color: colors.primary },
|
||||
sectionTitle: { fontSize: 20, fontWeight: 'bold', marginLeft: 20, marginTop: 20, marginBottom: 10 },
|
||||
participantCard: { backgroundColor: '#fff', marginHorizontal: 15, marginBottom: 10, borderRadius: 10, padding: 15, flexDirection: 'row', alignItems: 'center', elevation: 1 },
|
||||
participantInfo: { flex: 1 },
|
||||
participantName: { fontSize: 16, fontWeight: '600' },
|
||||
participantMobile: { fontSize: 14, color: '#6c757d', marginTop: 2 },
|
||||
paymentStatus: { alignItems: 'flex-end', marginHorizontal: 10 },
|
||||
statusText: { fontSize: 14, fontWeight: 'bold', textTransform: 'uppercase' },
|
||||
amountText: { fontSize: 14, color: '#495057', marginTop: 2 },
|
||||
lateFeeText: { fontSize: 13, color: colors.danger, fontWeight: '600', marginTop: 4 },
|
||||
waivedText: { fontSize: 13, color: colors.success, fontWeight: '600', marginTop: 4 },
|
||||
menuContainer: { justifyContent: 'center', alignItems: 'center', minWidth: 25 },
|
||||
modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center' },
|
||||
modalContainer: { width: '85%', backgroundColor: 'white', borderRadius: 15, padding: 20, elevation: 10 },
|
||||
modalTitle: { fontSize: 20, fontWeight: 'bold', color: '#333', marginBottom: 5 },
|
||||
modalSubtitle: { fontSize: 14, color: '#666', marginBottom: 20 },
|
||||
input: { width: '100%', borderWidth: 1, borderColor: '#ddd', borderRadius: 8, paddingHorizontal: 15, paddingVertical: 12, fontSize: 16, marginBottom: 20 },
|
||||
modalButtonContainer: { flexDirection: 'row', justifyContent: 'flex-end' },
|
||||
modalButton: { paddingVertical: 10, paddingHorizontal: 20, borderRadius: 8, marginLeft: 10 },
|
||||
cancelButton: { backgroundColor: '#f0f0f0' },
|
||||
cancelButtonText: { color: '#333', fontWeight: 'bold' },
|
||||
saveButton: { backgroundColor: colors.primary },
|
||||
saveButtonText: { color: 'white', fontWeight: 'bold' },
|
||||
});
|
||||
|
||||
export default Payments;
|
||||
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, FlatList, Dimensions, Alert, ActivityIndicator, Animated, Button } from 'react-native';
|
||||
import { apiCall } from '../../utils/ApiHelper';
|
||||
import { Ionicons } from "@react-native-vector-icons/ionicons";
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import ContributionDetails from '../ContributionScreen';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
const NUM_COLUMNS = 3;
|
||||
const ITEM_SIZE = width / NUM_COLUMNS - 20;
|
||||
|
||||
const RandomSelectUserScreen: React.FC<{ route: any }> = ({ route }) => {
|
||||
const navigation = useNavigation() as never
|
||||
const { groupId, cycleNumber } = route.params;
|
||||
const [users, setUsers] = useState<{ id: string; name: string }[]>([]);
|
||||
const [highlightedIndex, setHighlightedIndex] = useState<number | null>(null);
|
||||
const [winner, setWinner] = useState<{ id: string; name: string } | null>(null);
|
||||
const [isAnimating, setIsAnimating] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [openModal, setOpenModal] = useState(false)
|
||||
|
||||
const scaleAnim = useRef(new Animated.Value(1)).current;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchParticipants = async () => {
|
||||
try {
|
||||
const response = await apiCall(`https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/group/all/member?groupId=${groupId}`, "GET") as any;
|
||||
if (response.status === 'success' && response.data) {
|
||||
const formattedUsers = response.data.map((u: any) => ({
|
||||
id: u.id,
|
||||
name: `${u.firstName} ${u.lastName}`,
|
||||
}));
|
||||
setUsers(formattedUsers);
|
||||
} else {
|
||||
Alert.alert('Error', 'Failed to fetch participants.');
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert('Error', 'Could not connect to the server to fetch participants.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchParticipants();
|
||||
}, [groupId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (highlightedIndex !== null) {
|
||||
Animated.sequence([
|
||||
Animated.timing(scaleAnim, { toValue: 1.15, duration: 80, useNativeDriver: true }),
|
||||
Animated.timing(scaleAnim, { toValue: 1, duration: 80, useNativeDriver: true }),
|
||||
]).start();
|
||||
}
|
||||
}, [highlightedIndex]);
|
||||
|
||||
const handleStartSelection = () => {
|
||||
if (users.length === 0 || isAnimating) return;
|
||||
|
||||
setWinner(null);
|
||||
setIsAnimating(true);
|
||||
|
||||
let highlightCount = 0;
|
||||
const maxHighlights = 30 + Math.floor(Math.random() * 10); // Add some randomness
|
||||
|
||||
const highlightInterval = setInterval(() => {
|
||||
const randomIndex = Math.floor(Math.random() * users.length);
|
||||
setHighlightedIndex(randomIndex);
|
||||
highlightCount++;
|
||||
|
||||
if (highlightCount >= maxHighlights) {
|
||||
clearInterval(highlightInterval);
|
||||
fetchWinner();
|
||||
}
|
||||
}, 120);
|
||||
};
|
||||
|
||||
const fetchWinner = async () => {
|
||||
const payload = { groupId: groupId };
|
||||
try {
|
||||
const response = await apiCall('https://exotic-humpback-mistakenly.ngrok-free.app/api/fund/method/based/userSelect', 'POST', payload) as any;
|
||||
|
||||
if (response.status === 'success' && response.data?.id) {
|
||||
const winnerData = { id: response.data.id, name: `${response.data.firstName} ${response.data.lastName}` };
|
||||
const winnerIndex = users.findIndex(u => u.id === winnerData.id);
|
||||
|
||||
if (winnerIndex !== -1) {
|
||||
setWinner(winnerData);
|
||||
setHighlightedIndex(winnerIndex);
|
||||
} else {
|
||||
Alert.alert('Error', 'Winner not found among participants.');
|
||||
}
|
||||
} else {
|
||||
Alert.alert('Error', 'Failed to determine a winner from the API.');
|
||||
}
|
||||
} catch (error) {
|
||||
Alert.alert('Error', 'Could not connect to the server to fetch the winner.');
|
||||
} finally {
|
||||
setIsAnimating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = ({ item, index }: { item: { id: string; name: string }, index: number }) => {
|
||||
const isHighlighted = index === highlightedIndex;
|
||||
const isWinner = winner?.id === item.id && !isAnimating;
|
||||
|
||||
const animatedStyle = {
|
||||
transform: [{ scale: isHighlighted ? scaleAnim : 1 }],
|
||||
};
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
key={item.id}
|
||||
style={[
|
||||
styles.gridItem,
|
||||
isHighlighted && styles.highlightedItem,
|
||||
isWinner && styles.winnerItem,
|
||||
animatedStyle,
|
||||
]}
|
||||
>
|
||||
<Ionicons name="person" size={40} color={isHighlighted || isWinner ? '#000000ff' : '#4b5563'} />
|
||||
<Text style={[styles.gridText, (isHighlighted || isWinner) && styles.highlightedText]} numberOfLines={2}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View style={styles.centeredContainer}>
|
||||
<ActivityIndicator size="large" color="#4f46e5" />
|
||||
<Text style={styles.loadingText}>Loading Participants...</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Select a Winner</Text>
|
||||
<Text style={styles.subtitle}>Cycle #{cycleNumber}</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
data={users}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
numColumns={NUM_COLUMNS}
|
||||
extraData={{ highlightedIndex, winner }}
|
||||
contentContainerStyle={styles.grid}
|
||||
/>
|
||||
<View style={styles.footer}>
|
||||
{winner && !isAnimating && (
|
||||
<View style={styles.winnerTextContainer}>
|
||||
<Text style={styles.winnerText}>🎉 Winner: {winner.name}</Text>
|
||||
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Button title='Pay' onPress={() => setOpenModal(true)} />
|
||||
{
|
||||
openModal && <ContributionDetails data={undefined} visible={openModal} onClose={() => setOpenModal(false)} onConfirm={() => setOpenModal(false)} />
|
||||
}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.button, isAnimating && styles.buttonDisabled]}
|
||||
onPress={handleStartSelection}
|
||||
disabled={isAnimating}
|
||||
>
|
||||
<Text style={styles.buttonText}>{isAnimating ? 'Selecting...' : 'Start Selection'}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
centeredContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
loadingText: {
|
||||
marginTop: 15,
|
||||
fontSize: 18,
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
alignItems: 'center',
|
||||
},
|
||||
header: {
|
||||
paddingTop: 50,
|
||||
paddingBottom: 20,
|
||||
alignItems: 'center'
|
||||
},
|
||||
title: {
|
||||
fontSize: 32,
|
||||
fontWeight: 'bold',
|
||||
color: '#111827',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 18,
|
||||
color: '#6b7280',
|
||||
marginTop: 4
|
||||
},
|
||||
grid: {
|
||||
paddingHorizontal: 10,
|
||||
},
|
||||
gridItem: {
|
||||
margin: 8,
|
||||
width: ITEM_SIZE,
|
||||
height: ITEM_SIZE,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: 12,
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#e5e7eb',
|
||||
padding: 8,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
highlightedItem: {
|
||||
borderWidth: 5,
|
||||
borderColor: '#4f46e5',
|
||||
},
|
||||
winnerItem: {
|
||||
backgroundColor: '#10b981',
|
||||
borderColor: '#059669',
|
||||
},
|
||||
gridText: {
|
||||
marginTop: 8,
|
||||
color: '#1f2937',
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
fontSize: 12,
|
||||
},
|
||||
highlightedText: {
|
||||
color: '#000000ff',
|
||||
},
|
||||
footer: {
|
||||
width: '100%',
|
||||
padding: 20,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#e5e7eb',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fff'
|
||||
},
|
||||
button: {
|
||||
paddingVertical: 16,
|
||||
paddingHorizontal: 50,
|
||||
backgroundColor: '#4f46e5',
|
||||
borderRadius: 30,
|
||||
elevation: 3,
|
||||
shadowColor: "#4f46e5",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 4,
|
||||
},
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4.65,
|
||||
},
|
||||
buttonDisabled: {
|
||||
backgroundColor: '#9ca3af',
|
||||
shadowColor: 'transparent',
|
||||
},
|
||||
buttonText: {
|
||||
fontSize: 18,
|
||||
color: '#fff',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
winnerTextContainer: {
|
||||
marginBottom: 20,
|
||||
padding: 12,
|
||||
backgroundColor: '#d1fae5',
|
||||
borderRadius: 12,
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
winnerText: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: '#065f46',
|
||||
},
|
||||
});
|
||||
|
||||
export default RandomSelectUserScreen;
|
||||
@@ -36,10 +36,10 @@ export async function apiCall<T>(
|
||||
});
|
||||
|
||||
// --- Response Logging ---
|
||||
console.log(`--- API Response: [${response.status}] ${url} ---`);
|
||||
console.log(`--- API Response: [${response}] ${url} ---`);
|
||||
// Uncomment the line below to see the full response data in logs
|
||||
// console.log("Response Data:", JSON.stringify(response.data, null, 2));
|
||||
console.log("---------------------------------------");
|
||||
console.log("---------------------------------------",response);
|
||||
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
|
||||
Reference in New Issue
Block a user