payment cycle implementation

This commit is contained in:
Jaanu423
2025-09-29 11:31:48 +05:30
10 changed files with 624 additions and 2163 deletions
+83 -1936
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -30,6 +30,7 @@
"react-native-calendars": "^1.1313.0", "react-native-calendars": "^1.1313.0",
"react-native-dialog": "^9.3.0", "react-native-dialog": "^9.3.0",
"react-native-elements": "^3.4.3", "react-native-elements": "^3.4.3",
"react-native-fast-confetti": "^1.1.1",
"react-native-image-picker": "^8.2.1", "react-native-image-picker": "^8.2.1",
"react-native-keychain": "^10.0.0", "react-native-keychain": "^10.0.0",
"react-native-linear-gradient": "^2.8.3", "react-native-linear-gradient": "^2.8.3",
+3
View File
@@ -19,6 +19,7 @@ import ManageJoinedGroup from "../screens/management/ManageJoinedGroup";
import InviteScreen from "../screens/InviteScreen"; import InviteScreen from "../screens/InviteScreen";
import ManagementTabNavigation from "../screens/management/ManagementTabNavigation"; import ManagementTabNavigation from "../screens/management/ManagementTabNavigation";
import TransactionsList from "../screens/transaction/TransactionList"; import TransactionsList from "../screens/transaction/TransactionList";
import RandomSelectUserScreen from "../screens/management/RandomSelectUserScreen";
export type RootStackParamList = { export type RootStackParamList = {
Splash: undefined; Splash: undefined;
@@ -40,6 +41,7 @@ export type RootStackParamList = {
Invite: { groupId: string | number; code: string | number }; Invite: { groupId: string | number; code: string | number };
ManagementTabs:{ groupId: string | number;} ManagementTabs:{ groupId: string | number;}
TransactionList:undefined TransactionList:undefined
RandomSelectUserScreen: { groupId: string | number, cycleNumber: number };
}; };
const Stack = createNativeStackNavigator<RootStackParamList>(); const Stack = createNativeStackNavigator<RootStackParamList>();
@@ -92,6 +94,7 @@ const linking = {
<Stack.Screen name="Invite" component={InviteScreen} options={{ headerShown: false }} /> <Stack.Screen name="Invite" component={InviteScreen} options={{ headerShown: false }} />
<Stack.Screen name="ManagementTabs" component={ManagementTabNavigation} options={{ headerShown: false }} /> <Stack.Screen name="ManagementTabs" component={ManagementTabNavigation} options={{ headerShown: false }} />
<Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} /> <Stack.Screen name="TransactionList" component={TransactionsList} options={{ headerShown: false }} />
<Stack.Screen name="RandomSelectUserScreen" component={RandomSelectUserScreen} options={{ headerShown: false }} />
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );
-2
View File
@@ -79,8 +79,6 @@ const GroupCard = ({ item, onJoin, onView, isGrid }: { item: Group; onJoin: (gro
}; };
const FeaturedGroupCard = ({ item }: { item: Group }) => { const FeaturedGroupCard = ({ item }: { item: Group }) => {
console.log("image item : ", item)
console.log("image view : ", `data:image/jpeg;base64${item?.imageBlob}`)
return ( return (
<View style={styles.featuredCard}> <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} /> <Image source={{ uri: item?.imageBlob !== null ? `data:image/jpeg;base64${item?.imageBlob}` : `https://i.pravatar.cc/150?u=${item.id}` }} style={styles.featuredImage} />
+2 -2
View File
@@ -222,7 +222,7 @@ const Home: React.FC = () => {
return ( return (
<AppView barStyle="dark-content" style={styles.container}> <AppView barStyle="dark-content" style={styles.container}>
<View style={styles.header}> <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)}> <TouchableOpacity onPress={() => navigation.navigate("Profile" as never)}>
<Image <Image
source={ source={
@@ -270,7 +270,7 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
justifyContent: "space-around", justifyContent: "space-around",
}, },
greeting: { fontSize: 22, fontWeight: "600", color: colors.Home }, greeting: { fontSize: 22, fontWeight: "600", color: colors.pastelPink },
gridButton: { gridButton: {
flex: 1, flex: 1,
margin: 8, margin: 8,
+2 -1
View File
@@ -110,7 +110,8 @@ const UserProfileStatus: React.FC = () => {
{/* <Text style={styles.title}>Welcome Back 👋</Text> */} {/* <Text style={styles.title}>Welcome Back 👋</Text> */}
<LinearGradient <LinearGradient
colors={[colors.primary, colors.Home]} colors={[colors.pastelPink, colors.pastelPink
]}
start={{ x: 0, y: 0 }} start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }} end={{ x: 1, y: 1 }}
style={styles.card} style={styles.card}
+76 -24
View File
@@ -19,7 +19,7 @@ import AppView from '../../components/AppView';
import { Picker } from '@react-native-picker/picker'; import { Picker } from '@react-native-picker/picker';
import colors from '../../components/colors'; import colors from '../../components/colors';
import Clipboard from '@react-native-clipboard/clipboard'; 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 { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path import { ManagementTabParamList } from './ManagementTabNavigation'; // Adjust path
import { UPLOAD_GROUP_IMAGE } from '../../utils/ApiUrls'; 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 *as Keychain from 'react-native-keychain'
import Ionicons from '@react-native-vector-icons/ionicons'; import Ionicons from '@react-native-vector-icons/ionicons';
import { CustomButton } from '../../components/common/Custom'; import { CustomButton } from '../../components/common/Custom';
import { StackNavigationProp } from '@react-navigation/stack';
// --- Type Definitions --- // --- Type Definitions ---
interface Participant { interface Participant {
@@ -37,6 +39,7 @@ interface Participant {
role: 'ORGANIZER' | 'USER'; role: 'ORGANIZER' | 'USER';
} }
export interface GroupDetails { export interface GroupDetails {
id: string; id: string;
groupName: string; groupName: string;
@@ -61,6 +64,7 @@ export interface GroupDetails {
imageBlob: string imageBlob: string
} }
export interface EditGroupPayload { export interface EditGroupPayload {
groupName: string; groupName: string;
description: string; description: string;
@@ -74,17 +78,21 @@ export interface EditGroupPayload {
lateFeeValue: number | null; lateFeeValue: number | null;
} }
// Correct props for a screen inside a nested Tab Navigator // Correct props for a screen inside a nested Tab Navigator
type EditGroupScreenProps = CompositeScreenProps< type EditGroupScreenProps = CompositeScreenProps<
BottomTabScreenProps<ManagementTabParamList, 'EditGroup'>, BottomTabScreenProps<ManagementTabParamList, 'EditGroup'>,
NativeStackScreenProps<RootStackParamList> NativeStackScreenProps<RootStackParamList>
>; >;
type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null; type EditingCard = 'groupInfo' | 'financialRules' | 'configuration' | null;
type GroupManagementNavProp = StackNavigationProp<RootStackParamList, 'EditGroup'>;
// --- Main Screen Component --- // --- Main Screen Component ---
const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => { const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const { groupId } = route.params; const { groupId } = route.params;
const navigation = useNavigation<GroupManagementNavProp>();
const [editingCard, setEditingCard] = useState<EditingCard>(null); const [editingCard, setEditingCard] = useState<EditingCard>(null);
const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null); const [groupDetails, setGroupDetails] = useState<GroupDetails | null>(null);
@@ -106,10 +114,11 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
const [starting, setStarting] = useState(false); const [starting, setStarting] = useState(false);
const [inviting, setInviting] = useState(false); const [inviting, setInviting] = useState(false);
const [error, setError] = useState<string | null>(null); 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 [selectedImage, setSelectedImage] = useState<string | null>()
const [imageUpdating, setImageUpdating] = useState<boolean>(false) const [imageUpdating, setImageUpdating] = useState<boolean>(false)
// ... (All your handler functions: initializeEditableData, fetchGroupDetails, handleCancel, handleSave, etc. remain the same)
const initializeEditableData = (data: GroupDetails) => { const initializeEditableData = (data: GroupDetails) => {
setEditableData({ setEditableData({
groupName: data.groupName, groupName: data.groupName,
@@ -125,6 +134,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
}); });
}; };
const fetchGroupDetails = useCallback(async () => { const fetchGroupDetails = useCallback(async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -143,11 +153,22 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setLoading(false); setLoading(false);
} }
}, [groupId]); }, [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(() => { useEffect(() => {
fetchGroupDetails(); fetchGroupDetails();
getPaymentStatus();
}, [fetchGroupDetails]); }, [fetchGroupDetails]);
const handleCancel = () => { const handleCancel = () => {
if (groupDetails) { if (groupDetails) {
initializeEditableData(groupDetails); initializeEditableData(groupDetails);
@@ -155,9 +176,11 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setEditingCard(null); setEditingCard(null);
}; };
const handleSave = async () => { const handleSave = async () => {
if (!groupDetails) return; if (!groupDetails) return;
if (editableData.maxParticipants < groupDetails.totalParticipants) { if (editableData.maxParticipants < groupDetails.totalParticipants) {
Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`); Alert.alert('Invalid Input', `Max participants cannot be less than the current count (${groupDetails.totalParticipants}).`);
return; return;
@@ -175,10 +198,14 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
setSaving(false); setSaving(false);
} }
}; };
const handleDistribute = async () => {
navigation.navigate('RandomSelectUserScreen', { groupId: groupDetails?.id || '', cycleNumber: currentCycle || 0 });
};
const handleStartGroup = async () => { const handleStartGroup = async () => {
if (!groupDetails) return; if (!groupDetails) return;
Alert.alert( Alert.alert(
"Confirm Start", "Confirm Start",
"Are you sure you want to start this group? This action cannot be undone.", "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; if (!groupDetails) return;
setInviting(true); setInviting(true);
try { 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; const inviteLink = response.data;
@@ -230,12 +258,13 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
} }
}; };
const handleUpdateField = (field: keyof EditGroupPayload, value: any) => { const handleUpdateField = (field: keyof EditGroupPayload, value: any) => {
setEditableData(prev => ({ ...prev, [field]: value })); 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 commonInputProps = { style: styles.input, placeholderTextColor: "#9CA3AF" };
const getInputForField = () => { 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 renderCard = (title: string, fields: Array<{ field: keyof EditGroupPayload, label: string, displayValue: () => string | number }>, cardKey: EditingCard) => {
const isEditingThisCard = editingCard === cardKey; const isEditingThisCard = editingCard === cardKey;
return ( return (
<View style={styles.card}> <View style={styles.card}>
<View style={styles.cardHeader}> <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 (error) return <View style={styles.centered}><Text style={styles.errorText}>{error}</Text></View>;
if (!groupDetails) return null; if (!groupDetails) return null;
const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED'; const isEditable = groupDetails.status === 'PENDING' || groupDetails.status === 'APPROVED';
const isInvitable = groupDetails.status === 'APPROVED'; const isInvitable = groupDetails.status === 'APPROVED';
const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants; const canStartGroup = groupDetails.status === 'APPROVED' && groupDetails.totalParticipants >= groupDetails.minParticipants;
@@ -447,7 +478,6 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
return ( return (
// Use AppView as the root container with flex: 1
<AppView style={{ flex: 1, paddingHorizontal: 0 }}> <AppView style={{ flex: 1, paddingHorizontal: 0 }}>
<View style={styles.headerContainer}> <View style={styles.headerContainer}>
<Text style={styles.headerTitle}>Manage Group</Text> <Text style={styles.headerTitle}>Manage Group</Text>
@@ -458,6 +488,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
)} )}
</View> </View>
<ScrollView <ScrollView
style={styles.container} style={styles.container}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
@@ -490,20 +521,29 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
{ field: 'description', label: 'Description', displayValue: () => groupDetails.description }, { field: 'description', label: 'Description', displayValue: () => groupDetails.description },
], 'groupInfo')} ], 'groupInfo')}
{renderCard('Financial Rules', [
{ field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` }, {groupDetails.status === 'ACTIVE' ?
{ field: 'maxParticipants', label: 'Max Participants', displayValue: () => `${groupDetails.totalParticipants} / ${groupDetails.maxParticipants}` }, renderCard('Financial Rules', [
{ field: 'minParticipants', label: 'Min Participants', displayValue: () => `${groupDetails.minParticipants}` }, { field: 'contributionAmount', label: 'Contribution', displayValue: () => `₹${groupDetails.contributionAmount.toLocaleString('en-IN')}` },
{ field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` }, { field: 'latePaymentCharge', label: 'Late Fee', displayValue: () => `₹${(groupDetails.latePaymentCharge ?? 0).toLocaleString('en-IN')}` },
], 'financialRules')} { 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', [ {renderCard('Configuration', [
{ field: 'cycleType', label: 'Payment Cycle', displayValue: () => groupDetails.cycleType }, { 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 }, { field: 'visibility', label: 'Visibility', displayValue: () => groupDetails.visibility },
], 'configuration')} ], 'configuration')}
{/* Participants Card ... */}
<View style={styles.card}> <View style={styles.card}>
<Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text> <Text style={styles.cardTitle}>Participants ({groupDetails.totalParticipants})</Text>
{groupDetails.participants.length > 0 ? ( {groupDetails.participants.length > 0 ? (
@@ -524,8 +564,7 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
</View> </View>
</ScrollView> </ScrollView>
{/* --- FLOATING BUTTON AREA --- */}
{/* This View is a sibling of ScrollView, allowing it to float on top */}
{canStartGroup && ( {canStartGroup && (
<View style={styles.floatingButtonContainer}> <View style={styles.floatingButtonContainer}>
<TouchableOpacity <TouchableOpacity
@@ -541,14 +580,28 @@ const EditGroupScreen: React.FC<EditGroupScreenProps> = ({ route }) => {
</TouchableOpacity> </TouchableOpacity>
</View> </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> </AppView>
); );
}; };
// --- Styles ---
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#F3F4F6' }, 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 }, contentContainer: { padding: 16, paddingTop: 0, paddingBottom: 150 },
centered: { flex: 1, justifyContent: 'center', alignItems: 'center' }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' }, errorText: { color: '#EF4444', fontSize: 16, textAlign: 'center' },
@@ -619,15 +672,13 @@ const styles = StyleSheet.create({
cancelButton: { backgroundColor: '#E5E7EB' }, cancelButton: { backgroundColor: '#E5E7EB' },
inviteButton: { backgroundColor: colors.primary }, inviteButton: { backgroundColor: colors.primary },
buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' }, buttonText: { color: '#ffffffff', fontSize: 15, fontWeight: '600' },
// --- FLOATING BUTTON STYLES ---
floatingButtonContainer: { floatingButtonContainer: {
position: 'absolute', position: 'absolute',
bottom: 85, // Positioned above the tab bar bottom: 85,
left: 20, left: 20,
right: 20, right: 20,
alignItems: 'center', // Center the button horizontally alignItems: 'center',
zIndex: 1, // Ensure it's on top zIndex: 1,
}, },
activateButton: { activateButton: {
backgroundColor: colors.pastelOrange, backgroundColor: colors.pastelOrange,
@@ -639,7 +690,7 @@ const styles = StyleSheet.create({
shadowOffset: { width: 0, height: 4 }, shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3, shadowOpacity: 0.3,
shadowRadius: 4, shadowRadius: 4,
width: '90%', // Adjust width as needed width: '90%',
alignItems: 'center', alignItems: 'center',
}, },
activateButtonText: { activateButtonText: {
@@ -649,4 +700,5 @@ const styles = StyleSheet.create({
}, },
}); });
export default EditGroupScreen; export default EditGroupScreen;
+159 -196
View File
@@ -1,15 +1,15 @@
import React, { useEffect, useState, useCallback } from 'react'; import React, { useEffect, useState, useCallback } from 'react';
import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity } from 'react-native'; import { View, Text, StyleSheet, ActivityIndicator, ScrollView, RefreshControl, Alert, TouchableOpacity, Modal, TextInput } from 'react-native';
import { useRoute, useNavigation, RouteProp } from '@react-navigation/native'; import { useRoute, RouteProp } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { Ionicons } from "@react-native-vector-icons/ionicons"; import { Ionicons } from "@react-native-vector-icons/ionicons";
import colors from '../../components/colors'; import colors from '../../components/colors';
import AppView from '../../components/AppView'; import AppView from '../../components/AppView';
import { apiCall } from '../../utils/ApiHelper'; 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> { interface ApiResponse<T> {
status: 'success' | 'error'; status: 'success' | 'error';
data: T; data: T;
@@ -17,69 +17,54 @@ interface ApiResponse<T> {
statusCode: number; statusCode: number;
} }
// Type for an individual participant in the payment details
interface Participant { interface Participant {
userId: string; userId: string;
userName: string;
firstName: string; firstName: string;
lastName: string; lastName: string;
mobile: string; mobile: string;
contributionAmount: number; contributionAmount: number;
paidAmount: number; paidAmount: number;
payAmount?: number;
status: 'SUCCESS' | 'PENDING'; status: 'SUCCESS' | 'PENDING';
lateFee: number; lateFee: number;
daysLate: number;
lateFeeWaived: boolean; lateFeeWaived: boolean;
} }
// Fully typed payment details based on your JSON response
interface PaymentDetails { interface PaymentDetails {
groupId: string; groupId: string;
groupName: string; groupName: string;
currentCycle: number; currentCycle: number;
cycleType: string;
contributionAmount: number; contributionAmount: number;
totalContributedAmount: number;
lateFeeMode: string;
lateFeeValue: number;
notReceivedAmount: number;
totalLateFee: number;
totalDueWithLateFee: number; totalDueWithLateFee: number;
participants: Participant[]; 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 PaymentsRouteProp = RouteProp<PaymentStackParamList, 'Payments'>;
type PaymentsNavigationProp = StackNavigationProp<PaymentStackParamList>;
const Payments: React.FC = () => { const Payments: React.FC = () => {
const route = useRoute<PaymentsRouteProp>(); const route = useRoute<PaymentsRouteProp>();
const navigation = useNavigation<PaymentsNavigationProp>();
const { groupId } = route.params; const { groupId } = route.params;
const [paymentDetails, setPaymentDetails] = useState<PaymentDetails | null>(null); const [paymentDetails, setPaymentDetails] = useState<PaymentDetails | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false); 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 () => { const fetchPaymentDetails = useCallback(async () => {
if (!refreshing) setLoading(true); if (!refreshing) setLoading(true);
try { try {
const response = await apiCall( const response = await apiCall(`${API_BASE_URL}/current/payment/details/${groupId}`, 'GET') as ApiResponse<PaymentDetails>;
`https://exotic-humpback-mistakenly.ngrok-free.app/api/payments/current/payment/details/${groupId}`,
'GET'
) as ApiResponse<PaymentDetails>;
if (response && response.statusCode === 200) { if (response && response.statusCode === 200) {
setPaymentDetails(response.data); setPaymentDetails(response.data);
} else { } else {
Alert.alert("Error", response?.message || "Failed to fetch payment details."); Alert.alert("Error", response?.message || "Failed to fetch details.");
} }
} catch (error: any) { } catch (error: any) {
console.error("Fetch payment details error:", error);
Alert.alert("Error", error.message || "An unexpected error occurred."); Alert.alert("Error", error.message || "An unexpected error occurred.");
} finally { } finally {
setLoading(false); setLoading(false);
@@ -91,205 +76,183 @@ const Payments: React.FC = () => {
fetchPaymentDetails(); fetchPaymentDetails();
}, [fetchPaymentDetails]); }, [fetchPaymentDetails]);
const onRefresh = () => { const onRefresh = useCallback(() => {
setRefreshing(true); 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 renderParticipant = (participant: Participant) => {
const isPending = participant.status === 'PENDING'; const isPending = participant.status === 'PENDING';
const canWaive = isPending && participant.lateFee > 0;
return ( return (
<View key={participant.userId} style={styles.participantCard}> <View key={participant.userId} style={styles.participantCard}>
<View style={styles.participantInfo}> <View style={styles.participantInfo}>
<Text style={styles.participantName}>{participant.firstName} {participant.lastName}</Text> <Text style={styles.participantName}>{participant.firstName} {participant.lastName}</Text>
<Text style={styles.participantMobile}>{participant.mobile}</Text> <Text style={styles.participantMobile}>{participant.mobile}</Text>
</View> </View>
<View style={styles.paymentStatus}> <View style={styles.paymentStatus}>
<Text style={[ <Text style={[styles.statusText, { color: isPending ? colors.warning : colors.success }]}>
styles.statusText,
{ color: isPending ? '#e0d626ff' : '#18b107ff' }
]}>
{participant.status} {participant.status}
</Text> </Text>
<Text style={styles.amountText}> <Text style={styles.amountText}>
{isPending ?`₹${participant.payAmount}`:`₹${participant.paidAmount}`} {isPending ? `₹${participant.payAmount || 0}` : `₹${participant.paidAmount}`}
</Text> </Text>
{isPending && participant.lateFee > 0 && (
{/* --- NEW: Conditionally render late payment stats --- */} <Text style={styles.lateFeeText}>Late Fee: ₹{participant.lateFee}</Text>
{isPending && participant.daysLate > 0 && ( )}
<View style={styles.lateFeeContainer}> {participant.lateFee === 0 && participant.lateFeeWaived && <Text style={styles.waivedText}>Fee Waived</Text>}
<Text style={styles.lateFeeText}>Late Fee: ₹{participant.lateFee}</Text> </View>
<Text style={styles.lateFeeText}>Days Late: {participant.daysLate}</Text>
</View> <View style={styles.menuContainer}>
{canWaive && (
<TouchableOpacity onPress={() => openWaiverDialog(participant)}>
<Ionicons name="ellipsis-vertical" size={22} color="#6c757d" />
</TouchableOpacity>
)} )}
</View> </View>
</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 ( return (
<AppView paddingHorizontal={0}> <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 <ScrollView
style={styles.container} 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}> {loading && !refreshing ? (
<Text style={styles.headerTitle}>{paymentDetails.groupName}</Text> <ActivityIndicator style={{marginTop: 50}} size="large" color={colors.primary} />
</View> ) : (
<>
<View style={styles.summaryContainer}> <View style={styles.header}>
<View style={styles.summaryBox}> <Text style={styles.headerTitle}>{paymentDetails?.groupName}</Text>
<Text style={styles.summaryLabel}>Current Cycle</Text> </View>
<Text style={styles.summaryValue}>{paymentDetails.currentCycle}</Text> <View style={styles.summaryContainer}>
</View> <View style={styles.summaryBox}>
<View style={styles.summaryBox}> <Text style={styles.summaryLabel}>Cycle</Text>
<Text style={styles.summaryLabel}>Contribution</Text> <Text style={styles.summaryValue}>{paymentDetails?.currentCycle}</Text>
<Text style={styles.summaryValue}>₹{paymentDetails.contributionAmount}</Text> </View>
</View> <View style={styles.summaryBox}>
<View style={styles.summaryBox}> <Text style={styles.summaryLabel}>Contribution</Text>
<Text style={styles.summaryLabel}>Total Due</Text> <Text style={styles.summaryValue}>₹{paymentDetails?.contributionAmount}</Text>
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails.totalDueWithLateFee}</Text> </View>
</View> <View style={styles.summaryBox}>
</View> <Text style={styles.summaryLabel}>Total Due</Text>
<Text style={[styles.summaryValue, { color: colors.danger }]}>₹{paymentDetails?.totalDueWithLateFee}</Text>
<Text style={styles.sectionTitle}>Participants</Text> </View>
{paymentDetails.participants.map(renderParticipant)} </View>
<Text style={styles.sectionTitle}>Participants</Text>
{paymentDetails?.participants.map(renderParticipant)}
</>
)}
</ScrollView> </ScrollView>
</AppView> </AppView>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
centered: { container: { flex: 1, backgroundColor: '#f4f6f8' },
flex: 1, header: { padding: 20, paddingBottom: 15 },
justifyContent: 'center', headerTitle: { color: '#000000', fontSize: 24, fontWeight: 'bold' },
alignItems: 'center', summaryContainer: { flexDirection: 'row', justifyContent: 'space-around', padding: 20, backgroundColor: '#fff', marginHorizontal: 15, borderRadius: 12, elevation: 2 },
backgroundColor: '#f4f6f8', summaryBox: { alignItems: 'center' },
}, summaryLabel: { fontSize: 14, color: '#6c757d', marginBottom: 4 },
container: { summaryValue: { fontSize: 18, fontWeight: '700', color: colors.primary },
flex: 1, sectionTitle: { fontSize: 20, fontWeight: 'bold', marginLeft: 20, marginTop: 20, marginBottom: 10 },
backgroundColor: '#f4f6f8', participantCard: { backgroundColor: '#fff', marginHorizontal: 15, marginBottom: 10, borderRadius: 10, padding: 15, flexDirection: 'row', alignItems: 'center', elevation: 1 },
}, participantInfo: { flex: 1 },
header: { participantName: { fontSize: 16, fontWeight: '600' },
padding: 20, participantMobile: { fontSize: 14, color: '#6c757d', marginTop: 2 },
paddingBottom: 15, paymentStatus: { alignItems: 'flex-end', marginHorizontal: 10 },
}, statusText: { fontSize: 14, fontWeight: 'bold', textTransform: 'uppercase' },
headerTitle: { amountText: { fontSize: 14, color: '#495057', marginTop: 2 },
color: '#000000', lateFeeText: { fontSize: 13, color: colors.danger, fontWeight: '600', marginTop: 4 },
fontSize: 24, waivedText: { fontSize: 13, color: colors.success, fontWeight: '600', marginTop: 4 },
fontWeight: 'bold', menuContainer: { justifyContent: 'center', alignItems: 'center', minWidth: 25 },
}, modalOverlay: { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)', justifyContent: 'center', alignItems: 'center' },
summaryContainer: { modalContainer: { width: '85%', backgroundColor: 'white', borderRadius: 15, padding: 20, elevation: 10 },
flexDirection: 'row', modalTitle: { fontSize: 20, fontWeight: 'bold', color: '#333', marginBottom: 5 },
justifyContent: 'space-around', modalSubtitle: { fontSize: 14, color: '#666', marginBottom: 20 },
padding: 20, input: { width: '100%', borderWidth: 1, borderColor: '#ddd', borderRadius: 8, paddingHorizontal: 15, paddingVertical: 12, fontSize: 16, marginBottom: 20 },
backgroundColor: '#fff', modalButtonContainer: { flexDirection: 'row', justifyContent: 'flex-end' },
marginHorizontal: 15, modalButton: { paddingVertical: 10, paddingHorizontal: 20, borderRadius: 8, marginLeft: 10 },
borderRadius: 12, cancelButton: { backgroundColor: '#f0f0f0' },
elevation: 2, cancelButtonText: { color: '#333', fontWeight: 'bold' },
shadowColor: '#000', saveButton: { backgroundColor: colors.primary },
shadowOpacity: 0.05, saveButtonText: { color: 'white', fontWeight: 'bold' },
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',
},
}); });
export default Payments; 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;
+2 -2
View File
@@ -36,10 +36,10 @@ export async function apiCall<T>(
}); });
// --- Response Logging --- // --- 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 // Uncomment the line below to see the full response data in logs
// console.log("Response Data:", JSON.stringify(response.data, null, 2)); // console.log("Response Data:", JSON.stringify(response.data, null, 2));
console.log("---------------------------------------"); console.log("---------------------------------------",response);
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {